Skip to content

Instantly share code, notes, and snippets.

@5hyn3
Last active December 28, 2020 06:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 5hyn3/d75ccfc00df5a8eb77891edd7a5116c2 to your computer and use it in GitHub Desktop.
Save 5hyn3/d75ccfc00df5a8eb77891edd7a5116c2 to your computer and use it in GitHub Desktop.
declarative ui frameworks comparison

description

宣言的UIを実現するフレームワークであるFlutter, JetpackCompose, SwiftUIの比較。 以下のパターンで同じようなUIを実装してみて、そのコードの違いを比較する。

  • column pattern

    • "Hello,", "World!"の2つの文字列を縦に並べる
  • update pattern

    • ButtonとTextViewを縦に並べる。TextViewには初期状態で「0」が表示される。ButtonをタップするとTextViewに表示された数字がインクリメントされ表示される
    • Flutterに関しては「StatefulWidget」を利用したものと「Provider」を利用したものの両方を比較する
  • list pettern

    • 0,1,2,3...と要素の順番を表示したTextViewが無限に表示される無限スクロール可能なリスト
  • list with progress pettern

    • list petternに加え、「初期表示のローディング待ち時間にプログレス表示」「リスト末尾にプログレス表示」を行ったより実用的なパターン

検証環境

  • Flutter
    • IDE: AndroidStudio 4.1
    • Flutter: 1.22.0-12.1.pre
  • JetpackCompose
    • IDE: Android Studio Arctic Fox | 2020.3.1 Canary 3
    • Kotlin 1.4.21
    • JetpackCompose: 1.0.0-alpha09
    • paging-compose:1.0.0-alpha04
  • SwiftUI
    • IDE: Xcode 12.3
    • Swift5

参考

import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(body: ColumnPatternWidget()),
);
}
}
class ColumnPatternWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(children: [
Text('Hello,'),
Text('World!'),
]);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
returnF MaterialApp(
home: Scaffold(body: UpdatePatternWidget()),
);
}
}
class UpdatePatternWidget extends StatefulWidget {
@override
_UpdatePatternState createState() => _UpdatePatternState();
}
class _UpdatePatternState extends State<UpdatePatternWidget> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
Widget build(BuildContext context) {
return Column(
children: [
Text("$_count"),
FlatButton(
onPressed: _increment,
child: Text('increment'),
)
],
);
}
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(body: ProviderUpdatePatternWidget()),
);
}
}
class Counter with ChangeNotifier {
int value = 0;
increment() {
value++;
notifyListeners();
}
}
class ProviderUpdatePatternWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<Counter>.value(
value: Counter(),
child: ButtonAndText(),
);
}
}
class ButtonAndText extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(children: [
Text(context.select((Counter counter) => counter.value.toString())),
FlatButton(
onPressed: () => context.read<Counter>().increment(),
child: Text('increment'),
),
]);
}
}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(body: InfiniteListViewWidgetPage()),
);
}
}
class Numbers with ChangeNotifier {
List<int> numbers = [];
bool loading = false;
int page = 0;
final int pageSize = 20;
void load() async {
if (loading) {
return;
}
loading = true;
await Future.delayed(Duration(seconds: 3));
final list = List<int>.generate(pageSize, (i) => page * pageSize + i);
numbers.addAll(list);
page += 1;
loading = false;
notifyListeners();
}
}
class InfiniteListViewWidgetPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<Numbers>.value(
value: Numbers(),
child: InfiniteListViewWidget(),
);
}
}
class InfiniteListViewWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final length = context.watch<Numbers>().numbers.length;
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
if (index == length) {
context.read<Numbers>().load();
return null;
}
return ListCellWidget(index: index);
},
);
}
}
class ListCellWidget extends StatelessWidget {
final int index;
const ListCellWidget({Key key, this.index}) : super(key: key);
@override
Widget build(BuildContext context) {
final number = context.select((Numbers numbers) => numbers.numbers)[index];
return ListTile(
title: Text(number.toString()),
);
}
}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(body: InfiniteListViewWidgetPage()),
);
}
}
class Numbers with ChangeNotifier {
List<int> numbers = [];
bool loading = false;
int page = 0;
final int pageSize = 20;
void load() async {
if (loading) {
return;
}
loading = true;
await Future.delayed(Duration(seconds: 3));
final list = List<int>.generate(pageSize, (i) => page * pageSize + i);
numbers.addAll(list);
page += 1;
loading = false;
notifyListeners();
}
}
class InfiniteListViewWidgetPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<Numbers>.value(
value: Numbers(),
child: InfiniteListViewWidget(),
);
}
}
class InfiniteListViewWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final length = context.watch<Numbers>().numbers.length;
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
if (index == length) {
context.read<Numbers>().load();
return Center(
child: Container(
margin: const EdgeInsets.only(top: 8.0),
child: const CircularProgressIndicator(),
),
);
} else if (index > length) {
return null;
}
return ListCellWidget(index: index);
},
);
}
}
class ListCellWidget extends StatelessWidget {
final int index;
const ListCellWidget({Key key, this.index}) : super(key: key);
@override
Widget build(BuildContext context) {
final number = context.select((Numbers numbers) => numbers.numbers)[index];
return ListTile(
title: Text(number.toString()),
);
}
}
package com.example.tryjetpackcompose
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.tooling.preview.Preview
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ColumnPattern()
}
}
@Composable
fun ColumnPattern() {
Column {
BasicText(text = "Hello,")
BasicText(text = "World!")
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
ColumnPattern()
}
}
package com.example.tryjetpackcompose
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.Button
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.tooling.preview.Preview
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
UpdatePattern()
}
}
@Composable
fun UpdatePattern() {
val count = remember { mutableStateOf(0) }
Column {
BasicText(text = count.value.toString())
Button(onClick = { count.value++ }) {
BasicText(text = "increment")
}
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
UpdatePattern()
}
}
package com.example.tryjetpackcompose
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.tooling.preview.Preview
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingSource
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import kotlinx.coroutines.flow.Flow
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
InfiniteListViewPattern()
}
}
@Composable
fun InfiniteListViewPattern() {
val pageSize = 20
val source: Flow<PagingData<Int>> = Pager(PagingConfig(pageSize = pageSize)) {
object : PagingSource<Int, Int>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Int> {
val pageNumber = params.key ?: 0
return LoadResult.Page(
data = (pageSize * pageNumber until pageSize * pageNumber + pageSize).toList(),
prevKey = if (pageNumber > 0) pageNumber - 1 else null,
nextKey = pageNumber + 1
)
}
}
}.flow
val lazyItems = source.collectAsLazyPagingItems()
LazyColumn {
items(lazyItems) { item ->
BasicText(text = item.toString())
}
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
InfiniteListViewPattern()
}
}
package com.example.tryjetpackcompose
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.tooling.preview.Preview
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingSource
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
InfiniteListViewWithProgressPattern()
}
}
@Composable
fun InfiniteListViewWithProgressPattern() {
val pageSize = 20
val source: Flow<PagingData<Int>> = Pager(PagingConfig(pageSize = pageSize)) {
object : PagingSource<Int, Int>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Int> {
val pageNumber = params.key ?: 0
delay(3000)
return LoadResult.Page(
data = (pageSize * pageNumber until pageSize * pageNumber + pageSize).toList(),
prevKey = if (pageNumber > 0) pageNumber - 1 else null,
nextKey = pageNumber + 1
)
}
}
}.flow
val lazyItems = source.collectAsLazyPagingItems()
LazyColumn {
if (lazyItems.itemCount == 0) {
item {
CircularProgressIndicator()
}
}
items(lazyItems) { item ->
BasicText(text = item.toString())
}
if (lazyItems.itemCount != 0) {
item {
CircularProgressIndicator()
}
}
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
InfiniteListViewWithProgressPattern()
}
}
import SwiftUI
@main
struct TrySwiftUIApp: App {
var body: some Scene {
WindowGroup {
ColumnPattern()
}
}
}
struct ColumnPattern: View {
@ViewBuilder
public var body: some View {
VStack {
Text("Hello,")
Text("World!")
}
}
}
struct ColumnPattern_Previews: PreviewProvider {
static var previews: some View {
ColumnPattern()
}
}
import SwiftUI
@main
struct TrySwiftUIApp: App {
var body: some Scene {
WindowGroup {
UpdatePattern()
}
}
}
struct UpdatePattern: View {
@State var count: Int = 0
@ViewBuilder
public var body: some View {
VStack {
Text("\(count)")
Button("increment") { count += 1 }
}
}
}
struct UpdatePattern_Previews: PreviewProvider {
static var previews: some View {
UpdatePattern()
}
}
import SwiftUI
@main
struct TrySwiftUIApp: App {
var body: some Scene {
WindowGroup {
InfiniteListViewPattern()
}
}
}
struct InfiniteListViewPattern: View {
private let pageSize: Int = 20
@State private var items: [String] = []
@State private var isLoading: Bool = false
@State private var page: Int = 0
@ViewBuilder
public var body: some View {
List(items) { item in
VStack(alignment: .leading) {
Text(item)
}.onAppear {
if items.isLastItem(item) {
loadMoreItems()
}
}
}.onAppear {
if items.isEmpty {
loadMoreItems()
}
}
}
private func loadMoreItems() {
isLoading = true
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
defer {
page += 1
isLoading = false
}
if page == 0 {
items = Array(0...20).map { "\($0)" }
return
}
let maximum = ((page * pageSize) + pageSize) - 1
let moreItems: [String] = Array(items.count...maximum).map { "\($0)" }
items.append(contentsOf: moreItems)
}
}
}
extension RandomAccessCollection where Self.Element: Identifiable {
public func isLastItem<Item: Identifiable>(_ item: Item) -> Bool {
guard !isEmpty else {
return false
}
guard let itemIndex = lastIndex(where: { AnyHashable($0.id) == AnyHashable(item.id) }) else {
return false
}
let distance = self.distance(from: itemIndex, to: endIndex)
return distance == 1
}
}
extension String: Identifiable {
public var id: String {
return self
}
}
struct InfiniteListViewPattern_Previews: PreviewProvider {
static var previews: some View {
InfiniteListViewPattern()
}
}
import SwiftUI
@main
struct TrySwiftUIApp: App {
var body: some Scene {
WindowGroup {
InfiniteListViewWithProgressPattern()
}
}
}
struct InfiniteListViewWithProgressPattern: View {
private let pageSize: Int = 20
@State private var items: [String] = []
@State private var isLoading: Bool = false
@State private var page: Int = 0
@ViewBuilder
public var body: some View {
if items.isEmpty {
ProgressView().onAppear() {
loadMoreItems()
}
}
List(items) { item in
VStack(alignment: .leading) {
Text(item)
if isLoading && items.isLastItem(item) {
Divider()
ProgressView()
}
}.onAppear {
if items.isLastItem(item) {
loadMoreItems()
}
}
}
}
private func loadMoreItems() {
isLoading = true
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
defer {
page += 1
isLoading = false
}
if page == 0 {
items = Array(0...20).map { "\($0)" }
return
}
let maximum = ((page * pageSize) + pageSize) - 1
let moreItems: [String] = Array(items.count...maximum).map { "\($0)" }
items.append(contentsOf: moreItems)
}
}
}
extension RandomAccessCollection where Self.Element: Identifiable {
public func isLastItem<Item: Identifiable>(_ item: Item) -> Bool {
guard !isEmpty else {
return false
}
guard let itemIndex = lastIndex(where: { AnyHashable($0.id) == AnyHashable(item.id) }) else {
return false
}
let distance = self.distance(from: itemIndex, to: endIndex)
return distance == 1
}
}
extension String: Identifiable {
public var id: String {
return self
}
}
struct InfiniteListViewWithProgressPattern_Previews: PreviewProvider {
static var previews: some View {
InfiniteListViewWithProgressPattern()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment