Python版を踏まえて――Kotlin × Android で “Twitter 投稿アプリ” を作る

## はじめに
[前回の記事](https://qiita.com/fuha/items/9b0f664c13ebb186fac0)ではTwitter依存解消のため、 **Python + Tkinter** を使ってTwitterに投稿できるシンプルなデスクトップアプリを作成しました。今回はその延長として、**Androidアプリ** をKotlinで実装しました。  
タイムラインを閲覧せず、投稿だけをモバイルで完結させたいというニーズに対応するものです。

## 対象読者
- Android Studio が動く環境を持っている方  
- Twitterに投稿するシンプルな機能だけを使いたい方  

## ゴール・動作概要
- アプリ上でテキストを入力し、「ツイートする」ボタンを押すと投稿できる  
- Twitter API v2 の投稿に対応するため、jp.takke.twitter4j-v2 という非公式のラッパーライブラリを導入しています（Twitter4J の拡張版）。公式APIとは異なり一部制約や仕様差があるため、実運用では事前検証をおすすめします

![screenshot_20250507-053703_720.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987176/b99b843c-3615-4573-8908-7fc986b30194.png)



## 必要な準備

### 1. Twitter API キーの取得
Twitter Developer Portalにて、以下の情報を取得してください。

- Consumer Key  
- Consumer Secret  
- Access Token  
- Access Secret  

取得方法は「[Twitter API認証情報の取得方法](https://zenn.dev/rescuenow/articles/e8e896fd722d2d)」に書いてあります

> ※本稿ではコード中に直接記載していますが、実運用では `BuildConfig` や `local.properties` を使って外部管理することを推奨します。

### 2. 開発環境

| 項目 | 内容 |
|------|------|
| Android Studio | Meerkat 2024.3.1 Patch 1 |
| Kotlin | 2.0.21 |
| AGP (Android Gradle Plugin) | 8.9.1 |
| minSdk / targetSdk | 24 / 35 |

---

## プロジェクト構成
```
app/
├─ manifests/
│  └─ AndroidManifest.xml
├─ java/com/example/simpletweetpost/
│  ├─ MainActivity.kt
│  └─ test/ExampleUnitTest.kt
│  └─ androidTest/ExampleInstrumentedTest.kt
├─ res/
│  ├─ layout/activity_main.xml
│  ├─ values/
│  │  ├─ strings.xml
│  │  ├─ colors.xml
│  │  └─ themes.xml, themes.xml (night)
│  ├─ drawable/
│  └─ mipmap/
│      └─ ic_launcher などアイコン類
├─ build.gradle.kts
├─ libs.versions.toml
```
test / androidTest フォルダには自動生成されたテストファイルが含まれます。本アプリでは利用していませんが、ユニットテストやUIテストを導入する際のベースになります。

## 実装コード

### MainActivity.kt

```kotlin
package com.example.simpletweetpost

import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import twitter4j.TwitterFactory
import twitter4j.conf.ConfigurationBuilder
import twitter4j.v2 // 重要：拡張プロパティのインポート

class MainActivity : AppCompatActivity() {

    // Twitter API 認証情報
    private val CONSUMER_KEY    = "YOUR_CONSUMER_KEY"
    private val CONSUMER_SECRET = "YOUR_CONSUMER_SECRET"
    private val ACCESS_TOKEN    = "YOUR_ACCESS_TOKEN"
    private val ACCESS_SECRET   = "YOUR_ACCESS_SECRET"

    private lateinit var tweetEditText: EditText
    private lateinit var tweetButton: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // UIコンポーネントの取得
        tweetEditText = findViewById(R.id.tweetEditText)
        tweetButton = findViewById(R.id.tweetButton)

        // ツイートボタンのクリックリスナー
        tweetButton.setOnClickListener {
            val tweetText = tweetEditText.text.toString().trim()

            if (tweetText.isEmpty()) {
                showAlertDialog("警告", "ツイート内容が空です。入力してください。")
                return@setOnClickListener
            }

            postTweet(tweetText)
        }
    }

    private fun postTweet(message: String) {
        // UIスレッドをブロックしないようにコルーチンを使用
        CoroutineScope(Dispatchers.IO).launch {
            try {
                // Twitter認証設定
                val cb = ConfigurationBuilder()
                cb.setOAuthConsumerKey(CONSUMER_KEY)
                cb.setOAuthConsumerSecret(CONSUMER_SECRET)
                cb.setOAuthAccessToken(ACCESS_TOKEN)
                cb.setOAuthAccessTokenSecret(ACCESS_SECRET)
                cb.setJSONStoreEnabled(true) // JSONサポートを有効に

                val twitter = TwitterFactory(cb.build()).instance

                // Twitter API v2を使ってツイートを投稿
                val response = twitter.v2.createTweet(text = message)

                // UIスレッドに戻ってダイアログを表示
                withContext(Dispatchers.Main) {
                    showAlertDialog("成功", "ツイートが投稿されました！\nツイートID: ${response.id}")
                    tweetEditText.text.clear()
                }
            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    showAlertDialog("エラー", "ツイートの投稿中にエラーが発生しました: ${e.message}")
                }
            }
        }
    }

    private fun showAlertDialog(title: String, message: String) {
        AlertDialog.Builder(this)
            .setTitle(title)
            .setMessage(message)
            .setPositiveButton("OK", null)
            .show()
    }
}

```
（投稿成功後のダイアログを表示させない場合は showAlert() 呼び出しを削除）

### activity_main.xml
```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/labelTextView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="@string/title"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/tweetEditText"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_marginTop="8dp"
        android:background="@android:drawable/editbox_background"
        android:gravity="top|start"
        android:hint="@string/input_hint"
        android:inputType="textMultiLine"
        android:padding="8dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/labelTextView" />

    <Button
        android:id="@+id/tweetButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="@string/tweet_button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tweetEditText" />

</androidx.constraintlayout.widget.ConstraintLayout>
```
### strings.xml
```xml
<resources>
    <string name="app_name">SimpleTweetPost</string>
    <string name="input_hint">ここにツイート内容を入力</string>
    <string name="title">ツイート内容を入力してください:</string>
    <string name="tweet_button">ツイートする</string>
</resources>
```

### AndroidManifest.xml
```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.SimpleTweetPost"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
```
### build.gradle.kts（抜粋）
```kotlin
dependencies {
    implementation("org.twitter4j:twitter4j-core:4.0.7")
    implementation("io.github.takke:jp.takke.twitter4j-v2:1.4.1")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
    // その他の依存は libs.versions.toml に記載
}
```
## 実装のポイント
GUI: ConstraintLayout + EditText + Button の最小構成

認証処理: Twitter4J に API キーを設定して OAuth 認証

非同期処理: Coroutine で非同期処理を実装、UIスレッドをブロックしない

結果表示: 投稿完了／失敗を AlertDialog で通知

## ビルドと実行手順
必要に応じて API キーを環境に応じて埋め込む

Android Studio で実行

入力 → 投稿ボタン → 結果表示

## まとめ
Kotlin + Twitter4J でシンプルなTwitter投稿アプリが構築可能

投稿専用アプリなら、最小限の構成で十分動作する

実運用時はAPIキーの管理と投稿制限（Rate Limit）に注意すること



## 注意
無料プランでは、API経由での投稿上限が 1日あたり17件 に制限されています（2025/04/21時点）。上限を超える可能性がある場合は、有料プランの導入や投稿タイミングの調整を検討してください。