cutmail's blog

write the code

Claude CodeでAndroidアプリをモダンアーキテクチャに移行した話

はじめに

AndroidアプリのURL短縮アプリを、Claude Codeを使ってモダンなアーキテクチャに移行するプロジェクトを進めています。今回は、レガシーなView Systemから最新のJetpack Composeへの移行と、全体的なアーキテクチャの刷新について紹介します。

プロジェクト概要

  • 対象アプリ: Bitly APIを使用したURL短縮Androidアプリ
  • 技術スタック:
    • 旧: Java/Kotlin混在、View System、RxJava
    • 新: Kotlin、Jetpack Compose、Coroutines/Flow、Hilt、Room
  • アーキテクチャ: Clean Architecture(data / domain / presentation層)

実装した主要機能

1. ホーム画面のリニューアル

レガシーなView Systemから、Jetpack Composeを使用したモダンなUIに刷新しました。

Material Design 3への対応

@Composable
fun HomeScreen(
    initialUrl: String? = null,
    onNavigateToSettings: () -> Unit,
    viewModel: HomeViewModel = hiltViewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        // URL入力フィールド
        OutlinedTextField(
            value = uiState.inputUrl,
            onValueChange = viewModel::onUrlChanged,
            label = { Text(stringResource(R.string.url_input_label)) },
            placeholder = { Text(stringResource(R.string.url_input_placeholder)) },
            isError = uiState.error != null,
            enabled = !uiState.isLoading,
            modifier = Modifier.fillMaxWidth()
        )
        
        // 短縮ボタン
        Button(
            onClick = viewModel::onShortenUrl,
            enabled = uiState.inputUrl.isNotBlank() && !uiState.isLoading,
            modifier = Modifier.fillMaxWidth()
        ) {
            if (uiState.isLoading) {
                CircularProgressIndicator(modifier = Modifier.size(20.dp))
            } else {
                Text(stringResource(R.string.shorten_button))
            }
        }
    }
}

他アプリからの共有対応

// MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    val initialUrl = when (intent?.action) {
        Intent.ACTION_SEND -> {
            intent.getStringExtra(Intent.EXTRA_TEXT)
        }
        else -> null
    }
    
    setContent {
        URLShortenerTheme {
            MainNavigation(initialUrl = initialUrl)
        }
    }
}

2. 履歴・お気に入り機能の実装

Room データベースを活用した、履歴とお気に入り管理機能を実装しました。

データ層の実装

@Entity(tableName = "urls")
data class UrlEntity(
    @PrimaryKey
    val id: String,
    val longUrl: String,
    val shortUrl: String,
    val title: String? = null,
    val isFavorite: Boolean = false,
    val createdAt: Instant = Instant.now()
)

@Dao
interface UrlDao {
    @Query("SELECT * FROM urls ORDER BY createdAt DESC")
    fun getAllUrls(): Flow<List<UrlEntity>>
    
    @Query("SELECT * FROM urls WHERE isFavorite = 1 ORDER BY createdAt DESC")
    fun getFavoriteUrls(): Flow<List<UrlEntity>>
    
    @Query("""
        SELECT * FROM urls 
        WHERE LOWER(longUrl) LIKE '%' || LOWER(:query) || '%' 
           OR LOWER(COALESCE(title, '')) LIKE '%' || LOWER(:query) || '%' 
           OR LOWER(shortUrl) LIKE '%' || LOWER(:query) || '%' 
        ORDER BY createdAt DESC
    """)
    fun searchUrls(query: String): Flow<List<UrlEntity>>
    
    @Update
    suspend fun updateUrl(url: UrlEntity)
    
    @Delete
    suspend fun deleteUrl(url: UrlEntity)
}

検索機能の実装

検索UIの実装では、Material 3のSearchBarコンポーネントのオーバーレイ問題を解決しました。

@Composable
fun HistoryScreen(
    filter: HistoryFilter = HistoryFilter.ALL,
    viewModel: HistoryViewModel = hiltViewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
    Column {
        if (uiState.isSearchActive) {
            // カスタム検索ヘッダー(オーバーレイなし)
            TopAppBar(
                title = {
                    OutlinedTextField(
                        value = uiState.searchQuery,
                        onValueChange = viewModel::onSearchQueryChanged,
                        placeholder = { Text(stringResource(R.string.history_search_hint)) },
                        modifier = Modifier.fillMaxWidth(),
                        singleLine = true
                    )
                },
                navigationIcon = {
                    IconButton(onClick = { viewModel.onSearchActiveChanged(false) }) {
                        Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
                    }
                }
            )
        }
        
        // URL一覧
        LazyColumn {
            items(uiState.urls) { url ->
                UrlItem(
                    url = url,
                    onToggleFavorite = { viewModel.onToggleFavorite(url) },
                    onDelete = { viewModel.onDeleteUrl(url) }
                )
            }
        }
    }
}

3. ボトムナビゲーションの導入

アプリ全体のナビゲーション構造を改善し、主要機能へのアクセスを容易にしました。

@Composable
fun MainNavigation(initialUrl: String? = null) {
    val navController = rememberNavController()
    
    Scaffold(
        bottomBar = {
            NavigationBar {
                val items = listOf(
                    BottomNavItem.Home,
                    BottomNavItem.History,
                    BottomNavItem.Favorites,
                    BottomNavItem.Settings
                )
                
                items.forEach { item ->
                    NavigationBarItem(
                        icon = { Icon(item.icon, contentDescription = null) },
                        label = { Text(stringResource(item.titleRes)) },
                        selected = currentDestination?.hierarchy?.any { 
                            it.route == item.route 
                        } == true,
                        onClick = {
                            navController.navigate(item.route) {
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                launchSingleTop = true
                                restoreState = true
                            }
                        }
                    )
                }
            }
        }
    ) { paddingValues ->
        NavHost(
            navController = navController,
            startDestination = BottomNavItem.Home.route,
            modifier = Modifier.padding(paddingValues)
        ) {
            // 各画面の定義
        }
    }
}

4. 初回セットアップフローの実装

ユーザーオンボーディングを改善するため、初回起動時のセットアップフローを実装しました。

@Composable
fun SetupScreen(
    onSetupComplete: () -> Unit,
    onSetupLater: () -> Unit,
    viewModel: SettingsViewModel = hiltViewModel()
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(24.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        // デフォルトキーで始める(簡単スタート)
        Button(
            onClick = {
                viewModel.onSwitchToDefault()
                viewModel.markSetupCompleted()
                onSetupLater()
            },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(stringResource(R.string.use_default_key))
        }
        
        // または個人キーを設定
        OutlinedButton(
            onClick = { /* Bitlyサイトへ */ },
            modifier = Modifier.fillMaxWidth()
        ) {
            Icon(Icons.Default.OpenInNew, contentDescription = null)
            Spacer(modifier = Modifier.width(8.dp))
            Text(stringResource(R.string.get_token))
        }
    }
}

5. 設定画面の機能拡張

設定画面にアプリ情報とライセンス表示機能を追加しました。

アーキテクチャの変遷

Before: レガシーアーキテクチャ

旧アーキテクチャ(Single Activity + View System)
┌─────────────────────────────────────┐
│ MainActivity.kt (View System)       │
├─────────────────────────────────────┤
│ • 全ロジックが1つのActivity内      │
│ • findViewById でView取得          │
│ • RxJava による非同期処理          │
│ • 手動でライフサイクル管理          │
│ • グローバル状態管理なし            │
│ • APIトークンハードコーディング    │
└─────────────────────────────────────┘
// 旧実装例
class MainActivity : AppCompatActivity() {
    private lateinit var urlEditText: EditText
    private lateinit var shortenButton: Button
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        urlEditText = findViewById(R.id.url_edit_text)
        shortenButton = findViewById(R.id.shorten_button)
        
        shortenButton.setOnClickListener {
            // 直接API呼び出し
            BitlyClient.shorten(urlEditText.text.toString())
                .subscribe(
                    { result -> /* UIを直接更新 */ },
                    { error -> /* エラーハンドリング */ }
                )
        }
    }
}

After: 新モダンアーキテクチャ(Clean Architecture + Jetpack Compose)

graph TB
    subgraph "Presentation Layer"
        subgraph "UI (Jetpack Compose)"
            A[HomeScreen.kt]
            B[HistoryScreen.kt]
            C[SettingsScreen.kt]
            D[LicensesScreen.kt]
            E[Navigation.kt]
        end
        
        subgraph "ViewModels (MVVM)"
            F[HomeViewModel]
            G[HistoryViewModel]
            H[SettingsViewModel]
            I[LicensesViewModel]
        end
    end
    
    subgraph "Domain Layer"
        subgraph "Use Cases"
            J[ShortenUrlUseCase]
            K[GetHistoryUseCase]
            L[ToggleFavoriteUseCase]
            M[SearchUrlsUseCase]
            N[DeleteUrlUseCase]
        end
        
        subgraph "Models & Interfaces"
            O[ShortenedUrl]
            P[UrlShortenerRepository]
        end
    end
    
    subgraph "Data Layer"
        subgraph "Repository Implementation"
            Q[UrlShortenerRepositoryImpl]
        end
        
        subgraph "Data Sources"
            R[Remote: BitlyApiService]
            S[Local: Room Database]
            T[UrlDao]
            U[UrlEntity]
        end
        
        subgraph "Security & Utils"
            V[SecretManager]
            W[EncryptedSharedPreferences]
            X[AppInfoProvider]
        end
    end
    
    %% Dependencies
    A --> F
    B --> G
    C --> H
    D --> I
    
    F --> J
    G --> K
    G --> L
    G --> M
    G --> N
    H --> V
    I --> X
    
    J --> P
    K --> P
    L --> P
    M --> P
    N --> P
    
    P --> Q
    Q --> R
    Q --> S
    S --> T
    T --> U
    
    V --> W
    
    %% Styling
    classDef presentation fill:#e1f5fe
    classDef domain fill:#f3e5f5
    classDef data fill:#e8f5e8
    
    class A,B,C,D,E,F,G,H,I presentation
    class J,K,L,M,N,O,P domain
    class Q,R,S,T,U,V,W,X data

具体的な改善点

1. 依存性注入の導入

// Before: 手動でのオブジェクト生成
class MainActivity {
    private val bitlyClient = BitlyClient()
    private val sharedPreferences = getSharedPreferences("prefs", MODE_PRIVATE)
}

// After: Hiltによる依存性注入
@HiltViewModel
class HomeViewModel @Inject constructor(
    private val shortenUrlUseCase: ShortenUrlUseCase,
    private val secretManager: SecretManager
) : ViewModel()

2. State管理の改善

// Before: 手動でのState管理
class MainActivity {
    private var isLoading = false
    private var currentUrl = ""
    
    private fun updateUI() {
        shortenButton.isEnabled = !isLoading
        // 各Viewを手動で更新
    }
}

// After: Composeでの宣言的UI
@Composable
fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
    // State変更で自動的にUIが更新される
    Button(
        onClick = viewModel::onShortenUrl,
        enabled = !uiState.isLoading
    ) {
        if (uiState.isLoading) {
            CircularProgressIndicator()
        } else {
            Text("Shorten URL")
        }
    }
}

3. データ永続化の改善

// Before: データの永続化なし
// アプリ終了でデータが失われる

// After: Room データベース
@Entity(tableName = "urls")
data class UrlEntity(
    @PrimaryKey val id: String,
    val longUrl: String,
    val shortUrl: String,
    val isFavorite: Boolean = false,
    val createdAt: Instant = Instant.now()
)

@Dao
interface UrlDao {
    @Query("SELECT * FROM urls ORDER BY createdAt DESC")
    fun getAllUrls(): Flow<List<UrlEntity>>
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUrl(url: UrlEntity)
}

実装した詳細機能

1. アプリ情報表示機能

設定画面にアプリの詳細情報を表示する機能を追加しました。

AppInfoProvider の実装

@Singleton
class AppInfoProvider @Inject constructor(
    @ApplicationContext private val context: Context
) {
    fun getVersionName(): String {
        return try {
            val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
            packageInfo.versionName ?: "Unknown"
        } catch (e: PackageManager.NameNotFoundException) {
            "Unknown"
        }
    }
    
    fun getVersionCode(): Long {
        return try {
            val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                packageInfo.longVersionCode
            } else {
                @Suppress("DEPRECATION")
                packageInfo.versionCode.toLong()
            }
        } catch (e: PackageManager.NameNotFoundException) {
            0L
        }
    }
}

設定画面への統合

// About Section
Card(modifier = Modifier.fillMaxWidth()) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = stringResource(R.string.about),
            style = MaterialTheme.typography.titleMedium
        )
        
        InfoRow(
            label = stringResource(R.string.version_label),
            value = "$versionName ($versionCode)"
        )
        
        InfoRow(
            label = stringResource(R.string.build_type_label),
            value = buildType
        )
    }
}

2. ライブラリライセンス表示機能

OSS利用における適切なライセンス表示のため、使用ライブラリのライセンス一覧画面を実装しました。

データ構造

data class LibraryLicense(
    val name: String,
    val description: String,
    val licenseType: String,
    val licenseText: String
)

展開可能なライセンスカード

@Composable
private fun LicenseCard(license: LibraryLicense) {
    var isExpanded by remember { mutableStateOf(false) }
    
    Card(
        modifier = Modifier.fillMaxWidth(),
        onClick = { isExpanded = !isExpanded }
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(license.name, style = MaterialTheme.typography.titleMedium)
            Text(license.description, style = MaterialTheme.typography.bodyMedium)
            Text(license.licenseType, style = MaterialTheme.typography.labelMedium)
            
            if (isExpanded) {
                Text(
                    text = license.licenseText,
                    style = MaterialTheme.typography.bodySmall,
                    fontFamily = FontFamily.Monospace
                )
            }
        }
    }
}

Claude Codeの活用ポイント

1. 大規模リファクタリングの計画と実行

Claude Codeは、レガシーコードから最新のアーキテクチャへの移行において、段階的なアプローチを提案してくれました。

移行戦略:
1. 既存コードの分析と問題点の特定
2. Clean Architectureの導入(層分離)
3. View System → Jetpack Compose への段階的移行
4. RxJava → Coroutines/Flow への置き換え
5. 新機能追加による価値向上

2. アーキテクチャパターンの一貫性

既存のコードベースを理解し、適切なアーキテクチャパターンを維持:

  • Repository Pattern: データ層の抽象化
  • MVVM + Clean Architecture: 明確な責務分離
  • Use Case Pattern: ビジネスロジックの独立性
  • Dependency Injection: テスタビリティとメンテナンス性の向上

3. 実践的な問題解決

開発中に遭遇した具体的な問題に対して、実用的な解決策を提案:

SearchBarのオーバーレイ問題

// 問題: Material 3のSearchBarで画面がグレーアウト
// 解決: カスタムTopAppBar + OutlinedTextFieldで代替実装

APIキーモード切り替えの不具合

// 問題: 個人トークン未設定時にモード切り替えが失敗
// 解決: バリデーションタイミングの調整とフォールバック処理

4. コード品質の向上

  • Type-safe Navigation: Compose Navigationの活用
  • State Management: StateFlowとCollectAsStateWithLifecycleの適切な使用
  • Error Handling: 包括的なエラーハンドリング戦略
  • Testing: プレビュー関数の充実によるUI開発効率向上

5. 段階的な実装アプローチ

実装フェーズ:
Phase 1: Core Architecture (Domain + Data Layer)
Phase 2: Basic UI (Jetpack Compose基盤)
Phase 3: Navigation System (BottomNavigation)
Phase 4: Advanced Features (検索、お気に入り)
Phase 5: Polish & UX (設定画面、ライセンス)

開発効率とコード品質の向上

Before (手動開発)

  • 新機能追加: 2-3週間
  • バグ修正: 1-2日
  • アーキテクチャ改善: 困難
  • コードレビュー: 時間がかかる

After (Claude Code活用)

  • 新機能追加: 1-2日
  • バグ修正: 1-2時間
  • アーキテクチャ改善: 段階的に実施可能
  • コードレビュー: 一貫性のあるコードで効率的

学んだベストプラクティス

1. アーキテクチャ設計

  • Clean Architectureの実践的な適用方法
  • Jetpack Composeでの状態管理パターン
  • 依存性注入のベストプラクティス

2. UI/UX設計

3. 開発プロセス

  • 段階的なリファクタリング手法
  • プレビュー駆動開発による効率化
  • 多言語対応の計画的な実装

まとめ

Claude Codeを活用したAndroidアプリのモダン化プロジェクトを通して、以下の成果を得ることができました:

技術的成果

  • アーキテクチャ: レガシーコードからClean Architectureへの完全移行
  • UI Framework: View System → Jetpack Compose への移行完了
  • 非同期処理: RxJava → Coroutines/Flow への置き換え
  • データ管理: Room データベースによる永続化とオフライン対応

ビジネス価値

  • ユーザビリティ: 直感的なナビゲーションと検索機能
  • メンテナンス性: モジュール化されたコードベース
  • 拡張性: 新機能追加が容易なアーキテクチャ
  • 品質: 包括的なエラーハンドリングと多言語対応

開発効率

  • 開発速度: 新機能開発が大幅に高速化
  • コード品質: 一貫したアーキテクチャパターンの維持
  • チーム開発: 明確な責務分離による並行開発の促進

このプロジェクトは現在も継続中で、次のフェーズではテスト自動化とCI/CD パイプラインの改善を予定しています。Claude Codeは、モダンなAndroid開発において非常に強力なパートナーであることが実証されました。

2022年買ってよかったもの - テスラ編

このシリーズも3年目に突入しました。 最近テスラを納車された方も多くなってきたと思うので、やっていきましょう。

フロントマッドフラップ

shop.tesla.com

要は泥除けです。 自分はモバイルサービスの方に取り付けてもらいました。 短いのと長いのがありますが、長いのを取り付けています。

メリットとしては、つけたことによるボディ側面の走行による傷などが付きにくくなりました。 またこれは好みになってしまいますが、見た目を向上するアクセサリ的な役割もあるので、とても気に入っています。

デメリットとしては、全体からみた時に車高が下がるので、機械式駐車場でお断りされる率がかなり上がりました。 ここに関しては覚悟が必要です。

短いのだけにしたら良いかも。

イルミネーテッド キックプレート

shop.tesla.com

こちらも公式のアクセサリ。 少しお高いですが、つけることで暗闇でさりげなく光っておしゃれさを格段に上げてくれます。

ドアを開いた時だけ光るので、バッテリーもしばらくは持つ感じがします。 あんまりゴテゴテしたものをつけたくないけど、少しオリジナリティを付け加えたい人にはおすすめです。

Invisible Foldaway Car Mount with MagSafe Charger

Invisible Foldaway Car Mount with MagSafe Chargerwww.jowua-life.com

センターコンソールに取り付けてMagSafeで充電、取り付けもできるホルダー。 結構使っている人が多いですよね。

助手席に座っている人が快適にドライブ中に動画見たりできるし、使わないときには畳んでしまえるのでとても便利です。

STEM Portable

cado.com

美しいデザインの加湿器。 モデル3のドリンクホルダーに収まります(少しガタガタする)

水のタンクの容量が150mlと結構大きいので、一度水を入れるとしばらく足さなくても使えるのが便利です。 少しお値段張りますが、満足度はとても高いです。

加湿量も3段階で選べるのと、ライトの色も変えることができます。

車の中ってかなり乾燥するので、これが一台あると便利です。


それではまた来年!

(一ヶ月)Twitterやめてみた

自分はわりとTwitterに依存しているタイプだったのだが、 11月上旬に大きめなライフイベントがあったため、これを機にと思い一ヶ月程Twitterをやめてみた。

持っているすべてのスマートフォンMacなどからもTwitterアプリを削除した。

良かったこと

物事がよりクリアに見えるようになった

Twitterをしているとスクリーンの中のことに思考が占有されがち。

目の前で起きていることに自然と意識が向くようになった(当たり前)

余計なことを考えることがなくなったので、Twitterをしていた時よりも考える時間が増えた。

痩せた

副次的な効果もあると思うが、数値的にも落ちた。

Twitterをしていると、逐一食べ物の写真をあげてしまいがち。

スタバに行く頻度も減った。

その分、写真を撮ることがかなり減った。

良くなかったこと

世間の最新の話題に少し疎くなった

これはそうと言えばそうなんだけど、めちゃめちゃ困ったというほどでもなかった。 ニュースサイトやはてなブックマークなんかは以前と同じように良くみていたので、世の中でどんなことが起きているかは問題なく把握できた。

写真を撮る機会が減った

ここ一ヶ月で撮った写真がほぼない(笑 記録という意味では写真はたくさん残してあると良いですね。

まとめ

Twitterはほどほどに。 目の前で起きていることに目を向けよう。 今後は適度に距離を保ちながら、使っていきたいと思った所存。

2021年買ってよかったもの - テスラ周辺グッズ編

昨年に引き続き今年も買ってよかったシリーズ。 たくさんあるので、テスラの周辺グッズに留めようと思います。

センターコンソール トレイ

shop.tesla.com

アリエクとかでも似たようなものがたくさんありますが、さすが公式。 フロントとリア用で2個ついてきます。 フロント用はスライドすることができて、深い部分に収納したものも簡単に出せるのでかなり便利。 細々したものが整理できるので買ってよかったです。

ガラス ルーフ サンシェード

テスラは天井がガラスルーフなので夏は直射日光で正直暑いです。。 少しでも軽減できるかなと思って、買ってみました。 結果は暑さが軽減できて快適でした。 その反面ガラスルーフ越しに外の景色が見えなくなってしまいますが。。

shop.tesla.com

フットペダルカバー

自分はモデル3のロングレンジモデルなので、パフォーマンスモデルのように金属のフットペダルにしたくて購入。 取り付けがちょっと大変でしたが、かなり満足です。

ja.aliexpress.com

スリムゴミ箱

ゴミ箱は場所を取るのであまり大きいものを置きたくないですが、 これだとフロントシートの後ろに引っ掛けるだけなので、全然場所を取らず良いです。 比較的広がるので、大きめのゴミもわりと入ります。

空気清浄機

車の中って意外と空気が気になるので購入。 花粉症持ちですが、これがあると比較的くしゃみも少なくなりました。 常にONにしています。 カップホルダーにぴったりです。

ディフューザー

車の中はいい匂いがするようにしていたいので、色々探して購入。 アロマオイルを垂らすタイプなので、自分が好きな香りを使えます。 自分はヒノキの香りが好きなので、吉野ヒノキのオイルを使っています。 これもカップホルダーにハマります。 電池とUSBどちらでも対応。

store.at-aroma.com


他にも細かいものとかたくさん買ったけど、書ききれないのでこれくらいで。 また来年もテスラ関係のグッズはたくさん買うと思います。 サイバーホイッスル欲しかった(笑)

Instapaperを改めて使い始めた

あとで読む系のサービスは一貫してPocker (旧Read It Later)を使っていたのだけど、単純に追加した記事を読みたいと思った時に、 Instapaperの記事画面の方が読みやすく、ページが長い場合は自動で分割してくれるところがやっぱり良いと思い、Instapaperに戻ってきた。

しばらくはこれで運用してみようと思う。

2020年買って良かったもの

お題「#買って良かった2020

今年もやっていきます

サービス編

クラウド郵便箱atena

atena.life

私書箱のようなアドレスが付与されてそちらに郵便物が届くと、スキャンしたり転送したりしてくれるサービス。 自分は自宅と事務所どちらにも導入した。 リモートワークで事務所に行くことが減ったため、郵便物をスキャンして確認できるのは超便利。

途中で料金体系が変わって若干高くなったが、いいサービスだと思う。

青山フラワーマーケット 旬の花定期便

www.aoyamaflowermarket.com

定期的にお花を届けてくれる。 ボリュームもいくつかあって、ライフスタイルコースだと3種類のサイズの花が入っていて飾りやすい。 届ける周期も選べるので、自分の家にあった周期で頼むと忘れた頃にお花が届いて体験が良い。

ガジェット編

デロンギ(DeLonghi) コンパクト全自動コーヒーメーカー エレッタ

コーヒーメーカーはいくつか持っていたが、これを購入したことで圧倒的にコーヒーライフが楽になった。 まずコーヒーフィルターは不要で、お手入れも半自動。 肝心のコーヒーの味だが、その辺のカフェで出てくるのと同じクオリティが家で楽しめる。 エスプレッソ、カフェジャポーネ(アメリカン的なやつ)、カプチーノ、カフェラテなどメニューも豊富。 若干お高いが、リモートワーク生活がかなり豊かになる。

Anker Magnetic Cable Holder

机の上のケーブルがぐちゃぐちゃになったりしないので便利。

山崎実業 手荷物収納ボックス タワー

バックパックの置き場に困っていたが、帰宅してこれに突っ込めば床も汚くならないので買って良かった商品。 ブックシェルフとしても使えたり、使わない時は折り畳んで置けるので邪魔にならない。

テスラ モデル3 ロングレンジ

www.tesla.com

f:id:invent:20201231123229p:plain

今年は初めて車を購入した。
ロングレンジで、完全自動運転オプションもつけた。
日本だと今のところ自動車線変更、自動駐車くらいだが今後の徐々に解除されていくので楽しみ。
ソフトウェアアップデートがたまに降ってくるんだけど、これはiPhoneの体験と同じでソフトウェアによって体験が後から追加されていくのがすごい。
テスラの場合購入フローがめちゃくちゃアメリカンで、青山のテスラストアで一度試乗した後に、ネットでポチった。
引き渡しも、アプリから車の鍵を開錠して、セルフでピックアップする形だった。斬新。
サポートの電話が繋がりにくいなど色々あるけど、かなり満足している。

ちなみにこのリンクから購入すると1,500 km相当分のスーパーチャージャー無料充電特典がついてきます。 https://ts.la/tatsuya57881