Android mobile minne

ButterKnifeを minne Android から完全に削除しました

Android mobile minne

はじめに

minne 事業部でモバイルアプリエンジニアをしている @kyu です。 今回、minne Android において、長らく支えてくれたビューバインディング用のライブラリであるButterKnifeを削除しました。その経緯と作業内容を共有し、この変更から学んだことを共有したいと思います。

  1. はじめに
  2. ButterKnifeの概要
    1. ButterKnife導入の効果
  3. ButterKnife削除の背景と決断
    1. 削除が遅れた理由:
    2. 削除に至った動機:
  4. 削除対応方針と内容
    1. 削除対応方針
    2. 具体的な対応内容
  5. まとめと学び

ButterKnifeの概要

ButterKnifeは、Androidアプリ開発においてビューバインディングを手軽に行うために広く採用されていたライブラリです。 このツールは、XMLで定義されたビューとJavaやKotlinのコードをアノテーションを用いて結びつけ、冗長なコードを削減し、可読性と保守性を高める目的で使われました。

ButterKnife導入の効果

今更ではあるのですが、少しButterKnifeについて説明します。

導入前の一般的な処理:

以前は、onCreateメソッド内でfindViewByIdを使用してUIコンポーネントを手動で参照し、対応するフィールドに割り当てたり、イベントリスナーを設定する必要がありました。

public class MainActivity extends AppCompatActivity {

    private Button button;
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = findViewById(R.id.textView);
        button = findViewById(R.id.button);

        // ボタンが押下された処理
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                textView.setText("ボタンが押されました!");
            }
        });
    }
}

ButterKnife導入後のシンプルなコード:

ButterKnifeを使用すると、ビューのバインディングとイベントハンドリングがアノテーションベースで直感的に行えるようになり、コードがすっきりします。

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.button) Button button;
    @BindView(R.id.textView) TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
    }

    // ボタンが押下された処理
    @OnClick(R.id.button)
    public void onButtonClicked() {
        textView.setText("ボタンが押されました!");
    }
}

利用終了のお知らせ

ButterKnifeは非常に人気があったようですが、公式のAndroid Jetpack から代替となる ViewBinding と DataBinding が発表され、サードパーティ製のライブラリであった、ButterKnife はその需要が徐々に減少しました。 現在では開発も終了し、ButterKnife のリポジトリには ViewBinding や DataBinding の使用を推奨するようにも記載があります。

deprecated

ButterKnife削除の背景と決断

minneではButterKnifeの削除の必要性は認識されていましたが、実際の削除プロセスは遅れていました。その理由と、最終的に削除を決定した動機を説明します。

削除が遅れた理由:

  • 歴史あるアプリ: 新たにButterKnifeを採用することはなかったものの、minneは10年の歴史を持つアプリで、Javaで書かれた古いコードには依然としてButterKnifeが使用されていました。これらのコードを一度に全て更新する動きはありませんでした。
  • 優先順位: 画面の更新ではJetpack Composeへの移行やコードのKotlin化が優先されており、ButterKnifeの削除はこれらのタスクと一緒に行う方針でした。

削除に至った動機:

  • AGPのアップデート: 先日公開したこちらの記事の通りなのですが、Android Gradle Plugin (AGP) のバージョン更新の動きが高まり、その中でAGPの8系以降ではButterKnifeがサポートされないことが判明しました。
  • 必要性の認識: AGPを最新バージョンにアップデートし、より現代的な技術スタックへ移行するため、ButterKnifeの削除が必須となりました。

この経緯を経て、minneではButterKnifeの削除を決定し、よりモダンな技術への移行を進めました。

削除対応方針と内容

削除を開始した段階でButterKnifeを使用していた画面は20画面(ActivityやCustomViewなど)ありました。 意外にもコツコツ進めていたKotlin化でButterKnifeを削除をしていたので、思ったよりは少なくよかったです。

対応内容はとてもシンプルで難しいことはしておらず、ひたすらにViewBindingに置き換えていくだけです。 そして、対応方針と具体的な対応内容は以下の通りです。

削除対応方針

  • 画面の大幅な書き換えを避ける
    • Javaで書かれたコードはKotlin化しない
    • Kotlinで書かれたコードはJetpack Compose化しない
  • ButterKnifeからViewBindingへの移行のみを実施
  • 作業は一度に完了させるのではなく、段階的に行う

やること自体は何も難しいことはないので、方針が決まればあとはただひたすら実行するのみでした。

具体的な対応内容

1 ButterKnife削除が必要なViewのXMLに対して、ViewBindingを対応可能にするために、layoutタグの追加

// ViewBindingに移行するためのXMLの変更例...

// Before

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_below="@id/toolbar">
            
                // 省略...

            </RelativeLayout>
        </ScrollView>
</RelativeLayout>

// After

<layout> ← こちらを追加

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_below="@id/toolbar">

                // 省略...

            </RelativeLayout>
        </ScrollView>
    </RelativeLayout>
</layout>

2 BindingクラスをJava、Kotlinコード上で取得し必要なデータをセット

// ViewBindingに移行するためのXMLの変更例...

// Before
public class ShippingAddressSelectorActivity extends AppCompatActivityActivity {
    private final int mMenuGroupId = 0;

    @BindView(R.id.toolbar)
    Toolbar toolbar;

    @BindView(R.id.activity_shipping_address_selector_list)
    RecyclerView shippingAddressesView;

    @BindView(R.id.activity_shipping_address_selector_cautions_about_ordered)
    TextView cautionsTextView;

    @BindView(R.id.activity_shipping_address_selector_progress_bar)
    View progressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_shipping_address_selector);
        ButterKnife.bind(this);

        toolbar.setTitle(R.string.title_select_shipping_address);

        mShippingAddressesView.setLayoutManager(new LinearLayoutManager(this));

        ShippingAddressAdapter adapter = new ShippingAddressAdapter(this);
        shippingAddressesView.setAdapter(adapter);
        cautionsTextView.setText(R.string.caution_text);
    }
}

// After
public class ShippingAddressSelectorActivity extends AppCompatActivityActivity {
    private final int mMenuGroupId = 0;

    // Binding クラスの定義
    private ActivityShippingAddressSelectorBinding binding;    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = DataBindingUtil.setContentView(this, R.layout.activity_shipping_address_selector);

        binding.toolbar.setTitle(R.string.title_select_shipping_address);
        setSupportActionBar(binding.toolbar);
        if (getSupportActionBar() != null) {
            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        }

        binding.activityShippingAddressSelectorList.setLayoutManager(new LinearLayoutManager(this));    

        ShippingAddressAdapter adapter = new ShippingAddressAdapter(this);
        binding.activityShippingAddressSelectorList.setAdapter(adapter);
        binding.activityShippingAddressSelectorCautionsAboutOrdered.setText(R.string.caution_text);
    }
}

以上が、実際の変更内容になります。

余談で、唐突ではありますが、この対応以外にも minne 内で使用されている Epoxy というライブラリがButterKnifeが生成するR2クラスを使用しており、 EpoxyからButterKnifeを剥がす対応がまた一癖あったのですが、本当はこれがViewBindingに置き換えるより大変でした。。

まとめと学び

これにてminneのAndroidアプリからButterKnifeを完全に排除することができました。 約20画面に及ぶButterKnifeの置換作業でしたが、ViewBindingへの移行をPRを分けて4回に分割し、慎重に進めることができました。 当時新しい技術として登場し、minne Android に長らく貢献してくれた ButterKnife には、本当に感謝の気持ちでいっぱいです。

また、ButterKnifeを削除する過程で得た学びは簡単ですが、以下の通りです。

  1. 技術の進化に適応する: ButterKnifeはかつては便利なライブラリでしたが、技術は進化し続ける。ViewBindingやDataBindingのような新しい技術が登場したとき、それに適応し、古い方法から移行し続けることが重要である。

  2. 段階的なアプローチ: 当たり前ですが、大きな変更は一気に行うよりも、小出しに分割して段階的に進める方が現実的で、対応が進めやすくなる。

  3. 集中と単純化: 特定の目的(この場合はButterKnifeの削除)に焦点を当て、目標をシンプルに保つことで、作業はより管理しやすく、実行しやすくなる。

対応の開始前は大変そうに感じていましたが、実際には思ったよりスムーズに進んでよかったです。 あまりないとは思いますが、もしButterKnifeをまだ使っているプロジェクトがあり、これを読んでいただけていたら、この機会に削除を考えてみてはどうでしょうか。作業は意外とシンプルで、これがきっかけでより現代的な技術スタックに移行する一歩になるかもしれません。

最後にminneのAndroidアプリ開発に関心がある方、まだまだminneには課題が山積しています、ぜひ力を貸していただける方がいましたらお声かけください。