Androidネイティブの実装をFlutterでリプレースしたらコード量がすごく減った話

# はじめに
この記事を書くことになった経緯としては、最初にJavaで実装を進めていました。
ですが、開発途中で宣言的UIのフレームワークの良さや凄さを知る機会があり、それによりFlutterを勉強してFlutteで実装したいと思い始めました。
そこで、今作っているものをFlutterで書いたらどうなるのかと思い立ちFlutterでの実装も進めました。
結果として、同じ機能をFlutterで実装してみたところ驚くほど少ないコード量で実装できたので、せっかくだと思い記事にまとめることにしました。
# 紹介する実装の部分
__「Firestoreから取得したデータをGridViewで表示する実装」__
の部分について紹介していきたいと思います。
Androidネイティブの方の実装ではMVVMモデルでクラス分けをした実装になっていますが、Flutterの方はまだ勉強して日が浅いためアーキテクチャモデルまで意識した作りにはなっていなかったりします。
なので実装をするアーキテクチャの前提は違うかもしれませんが、同じ機能を実現しようとした時にどんな感じで実装が出来るのかという視点で見てもらえれば良いかと思います。

# 完成画面イメージ
Firestoreにお酒の名前の一覧のデータを置いておき、そのデータを取得してグリッドで表示するというごく簡単なものです。
左がAndroidネイティブで右がFlutterです。
（獺祭あり過ぎですね・・・）
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545703/e0addcc9-b393-c2cd-9ac4-479ebcb22ebb.png" width=40%/>　　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545703/5a134fa3-eed5-71ae-3a44-db338beaee08.png" width=40%/>

# Androidネイティブでの実装
Androidネイティブでの実装ではJavaで書いたコードで紹介していきたいと思います。１点目は画面レイアウトの定義、２点目はそのレイアウトを利用した実装についてです。
## 画面レイアウトの定義
まず、１つ１つのアイテムをグリッド形式で表示するためのレイアウトを定義します。
CardViewのウィジェットを利用することで簡単に角丸のグリッドアイテムが作成できます。

```grid_myitem.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
    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:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:cardCornerRadius="6dp"
    app:cardElevation="6dp"
    app:contentPadding="8dp"
    app:cardUseCompatPadding="true">
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:ignore="UseCompoundDrawables">
        <ImageView
            android:id="@+id/myitem_image_view"
            android:layout_width="match_parent"
            android:layout_height="160dp"
            android:contentDescription="@string/myitem_image" />

        <TextView
            android:id="@+id/myitem_text_view"
            android:textSize="20sp"
            android:textColor="@android:color/black"
            android:textAlignment="center"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    </LinearLayout>
</androidx.cardview.widget.CardView>
```
次に、上記で定義したアイテムをグリッド形式で表示するために、FragmentにRecyclerViewを定義します。

```fragment_home.xml
・・・
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/myitem_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:ignore="MissingConstraints" />
・・・
```

## Firestoreから取得したデータを表示する実装
上記の画面レイアウトを利用して画面に表示するまでの実装を以下に書いていきます。
まず、CardViewをRecyclerViewにinflateする処理を実装します。

```HomeFragnemt.java
public class HomeFragment extends Fragment {

    private HomeViewModel homeViewModel;
    private FragmentHomeBinding binding;

    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {
        homeViewModel = new ViewModelProvider(this).get(HomeViewModel.class);

        binding = FragmentHomeBinding.inflate(inflater, container, false);
        View root = binding.getRoot();

        final RecyclerView recyclerView = binding.myitemRecyclerView;
        HomeListAdapter homeListAdapter = new HomeListAdapter(new HomeListAdapter.HomeListDiff(), this::onAdapterClicked);
        recyclerView.setAdapter(homeListAdapter);
        recyclerView.setLayoutManager(new GridLayoutManager(getContext(), 3, RecyclerView.VERTICAL, false));

        homeViewModel.getBrandIdentifierList().observe(getViewLifecycleOwner(), brandIdentifiers -> homeListAdapter.submitList(brandIdentifiers));
    }
}
```

次に、Firestoreから取得したデータをMVVMモデルでRecyclerViewにバインドするまでの実装を書きます。
まずは、RecyclerViewに設定するアダプターの定義をして・・・。

```HomeListAdapter.java
public class HomeListAdapter extends ListAdapter<BrandIdentifier, HomeViewHolder> {
    protected HomeListAdapter(@NonNull @NotNull DiffUtil.ItemCallback<BrandIdentifier> diffCallback) {
        super(diffCallback);
    }

    @NonNull
    @Override
    public HomeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.grid_myitem, parent, false);
        return new HomeViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull HomeViewHolder holder, int position) {
        String id = getItem(position).getId();
        String name = getItem(position).getTitle();
        holder.bind(id, name);
    }

    public static class HomeListDiff extends DiffUtil.ItemCallback<BrandIdentifier> {

        @Override
        public boolean areItemsTheSame(@NonNull @NotNull BrandIdentifier oldItem, @NonNull @NotNull BrandIdentifier newItem) {
            return false;
        }

        @Override
        public boolean areContentsTheSame(@NonNull @NotNull BrandIdentifier oldItem, @NonNull @NotNull BrandIdentifier newItem) {
            return false;
        }
    }
}
```
次に、アダプターに設定するデータホルダーを定義します。

```HomeViewHolder.java
public class HomeViewHolder extends RecyclerView.ViewHolder {

    private String mId;
    private ImageView mMyItemImage;
    private TextView mMyItemText;

    public HomeViewHolder(@NonNull @NotNull View itemView) {
        super(itemView);
        mMyItemImage = (ImageView) itemView.findViewById(R.id.myitem_image_view);
        mMyItemText = (TextView) itemView.findViewById(R.id.myitem_text_view);
    }

    public void bind(String id, String name) {
        mId = id;
        mMyItemImage.setImageResource(R.drawable.ic_ponshu);
        mMyItemText.setText(name);
    }
}
```
そしてバインドするためのViewModelを実装して・・・。

```HomeViewModel.java
public class HomeViewModel extends AndroidViewModel {
    private PonshuRepository mRepository;

    public HomeViewModel(@NonNull Application application) {
        super(application);
        mRepository = PonshuRepository.getInstance();
    }

    public LiveData<List<BrandIdentifier>> getBrandIdentifierList() {
        return mRepository.getBrandIdentifierList();
    }
}
```
最後に、Firestoreからデータを取得するリポジトリを実装します。

```PonshuRepository.java
public class PonshuRepository {
    private static PonshuRepository mInstance;

    private FirebaseFirestore mDb;
    private CollectionReference mCollectionRef;

    private MutableLiveData<List<BrandIdentifier>> mBrandNameList;

    public static PonshuRepository getInstance() {
        if (mInstance == null) {
            mInstance = new PonshuRepository();
        }
        return mInstance;
    }

    private PonshuRepository() {
        mDb = FirebaseFirestore.getInstance();
        mCollectionRef = mDb.collection("BrandList");
    }

    public LiveData<List<BrandIdentifier>> getBrandIdentifierList() {
        if (mBrandNameList == null) {
            mBrandNameList = new MutableLiveData<>();
        }
        mCollectionRef.get()
                .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                    @Override
                    public void onComplete(@NonNull Task<QuerySnapshot> task) {
                        if (task.isSuccessful()) {
                            List<BrandIdentifier> brandIdentifierList = new ArrayList<>();
                            for (QueryDocumentSnapshot document : task.getResult()) {
                                Map<String, Object> brandData = (Map<String, Object>) document.getData();
                                brandIdentifierList.add(new BrandIdentifier(document.getId(), (String) brandData.get("title")));
                            }
                            mBrandNameList.setValue(brandIdentifierList);
                        } else {
                            Log.d("FIREBASE", task.getException().toString());
                        }
                    }
                });
        return mBrandNameList;
    }
}
```

# Flutterでの実装
Flutterでの実装は、これから紹介する１ファイルだけの実装になります。
Flutterは宣言的UIのフレームワークであるため、上記で紹介したようなレイアウト用のXMLファイルと機能の実装ファイルを分ける事なく、レイアウト＋実装がまとめて書けます。なので、

 + レイアウト部分についてはGridView.builderを利用してグリッドを生成
 + Firestoreからのデータ取得をStreamBuilderで取得

となります。実装は以下の通りです。

```home_view.dart
class SakeHomeViewWidget extends StatelessWidget {
  final Color color;
  final String title;

  const SakeHomeViewWidget({Key? key, required this.color, required this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {

    var assetsImage = "images/ic_sake.png";
    return Scaffold(
      body: StreamBuilder(
        stream: FirebaseFirestore.instance.collection('Brands').snapshots(),
        builder: (BuildContext context,
          AsyncSnapshot<QuerySnapshot> snapshot) {
          if (!snapshot.hasData) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }

          return GridView.builder(
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3,
                crossAxisSpacing: 10.0, // 縦
                mainAxisSpacing: 10.0, // 横
                childAspectRatio: 0.7),
            itemCount: snapshot.data!.docs.length,
            padding: const EdgeInsets.all(5.0),
            itemBuilder: (BuildContext context, int index) {
              return Container(
                child: GestureDetector(
                  onTap: () {},
                  child: Column(
                    children: <Widget>[
                      Image.asset(assetsImage, fit: BoxFit.cover,),
                      Container(
                        margin: const EdgeInsets.fromLTRB(8, 8, 8, 0),
                        child: Text(
                          snapshot.data!.docs[index]['title'],
                          maxLines: 2,
                          overflow: TextOverflow.ellipsis,),
                      ),
                    ],
                  )),
                padding: const EdgeInsets.all(10.0),
                decoration: BoxDecoration(
                  color: color,
                  borderRadius: BorderRadius.circular(10),
                  boxShadow: const [
                    BoxShadow(
                      color: Colors.grey,
                      offset: Offset(5.0, 5.0),
                      blurRadius: 10.0,
                    )
                  ],
                ),
              );
            },
          );
        }),
    );
  }
}
```

基本的にはこれだけで実装ができてしまいます。非常に短いですね。
同じ機能をこれだけ短いコード量で実現出来るのであれば、Flutter勉強しようかなって気になりませんか？

# おわりに
いかがでしたでしょうか。Androidネイティブ（特にJava）で書いていたものをFlutterにするとこれだけコード量が少なく、同じ機能が実現できました。
Androidネイティブの開発でもJavaで書くのではなくKotlinを使う、Jetpack Composeを使う等をすればもっとコード量を少なく書くことはできます。
この記事で言いたかったのは、
__「宣言的UIのフレームワークはモバイルアプリ開発を行う上で非常にメリットのあるものなんだ」__
と言うことでした。
なので今後もFlutterの勉強を進めて、様々なライブラリを使いこなせるようになったりアーキテクチャを採用した設計にしていきたいと思います。