あおぶろ

スマホアプリ開発のこととか

2021年のふり返り

2年ぶりに実家に帰省しました!
両親もいい年なので、がっつり体調崩してたりしてたら怖いな、と思ってましたが元気そうで安心しました。

今年は環境的に大きな変化はありませんでしたが、これまでよりは少しは充実した1年になったかな〜と思ってます

2021年にやったことのふり返りと、来年やりたいことを書いていきます。

今年やったこと

個人開発

Flutter、iOSAndroidでそれぞれアプリを作りました。

aocm214.hatenablog.com

aocm214.hatenablog.com

aocm214.hatenablog.com

今年に入るまで、ほとんど自分のやったことを見える形でアウトプットしていませんでした。
これ結構まずいんじゃない?という危機感もあり、分かりやすいアウトプットが欲しくなり、作りました。

Flutterのアプリ開発体験はとても良く、もっとFlutterをやりたかったのですが、本業でiOSの保守を少し任されたり、キャリアの割りにAndroid詳しくなくないか?と不安を覚えたりと色々迷走した結果、この結果となりました。

技術的なキャッチアップのためにはじめたことですが、思ったよりインストールされて嬉しい

f:id:aoshima214:20211231202343p:plain:w300

ブログ

このブログです。 10月に始めて、11記事書きました。

始めた目的は以下に書いてあります。

aocm214.hatenablog.com

が、正直なことをいうと後述する転職活動のためでもあります。

技術力のアピールというより、こういう文章を書く人なんだな〜みたいなパーソナリティの部分とか、こういうことに興味があるんだな〜みたいな興味関心の部分を判断する材料があったほうが、ミスマッチが少なくなるんじゃないかなという打算的な思惑によってこのブログが生まれました。

気軽に文章を投稿できる場所があることで、アウトプットの題材を常に探すようになった...ような気がする

やってみると楽しいので、ブログやろうか迷っている人、おすすめです。

転職活動

未経験で入った今の会社で働いてそろそろ3年になり、色々思うところもあり転職活動してます。

11月くらいまでに4つくらい転職サイトに登録して、12月からカジュアル面談をいくつか受けました。
(選考も1個受けて落ちました)

色んな転職サイトがあるな〜とか、カジュアル面談ってこういう感じなのか〜とか、思っていたより並行して受けられないな〜とか色々と学びがあります。

最初はAndroidだけで転職を考えていましたが、思ったよりFlutterの求人もあることに気づき、 そちらにも興味が引っ張られています。
(まだまだFlutterで転職できるほど求人数ないでしょって思ってた)

来年も引き続き受けていきます!

来年やりたいこと

引越し

今住んでいる場所が6畳ワンルームの限界アパートなので、転職と同時に引っ越します。
とりあえずまともな背丈の冷蔵庫を置きたい。

社外発信

ブログ以外の、人目につく場所に少しずつ記事を書いていきたいです。
あとDroidKaigiかFlutterKaigiのトークに何かで応募したい。

個人開発

今年のアウトプットが継続できたのは、個人開発が趣味としても楽しめたのが大きかったかなと思ってます。

来年は丁寧に愛着がわくアプリをつくりたいです。

運動

結局これが一番大事。
ランニングの頻度を増やして身体を引き締めていきたい。


以上! 来年も楽しく過ごせますように(‐人‐)

Flutterのテンプレートリポジトリをつくる

前回Flutterでアプリを作ったときは、とにかく最短でアプリをリリースすることを目標にしていたので、環境分けとか多言語化とかなんにも考えてませんでした。

今回Flutterの初期設定とかビルド周りの理解も兼ねて、Flutterプロジェクト用のテンプレートリポジトリを作ってみました。

作ったもの

github.com

このテンプレートの機能

Flutterのバージョン管理

asdfで管理することにしました。

f:id:aoshima214:20211231012034p:plain:w300

dart-defineによる環境分け

FlutterでDart-defineのみを使って開発環境と本番環境を分ける

↑の記事を参考に、dev, stg, product の3つを作成しました

環境 ビルドタイプ コマンド
dev debug flutter run --dart-define=FLAVOR=dev
stg release flutter run --release --dart-define=FLAVOR=stg
product release flutter run --release --dart-define=FLAVOR=product

以下を、dart-defineで渡されるFlavorによって分けるよう設定しました

  • アプリのID(applicationId,Bundle ID)
  • アプリのアイコン
  • Firebase

f:id:aoshima214:20211231012241p:plain:w300

ホームに表示されるアプリ名の後ろに .devとか .stgをつけるのは以下の理由でやめました。

  • アプリ名を多言語化したい場合、記事の方法だと対応できない
  • アイコンで判別がつけば十分

言語化

とりあえず日本語・英語に対応できるようにしました。

f:id:aoshima214:20211231012359p:plain

Linter

Flutter2.5.0以降は、最初からflutter_lintsがプロジェクトに入っています。
mono氏がこれをベースにルールを追加した、pedantic_monoを使用します。

pedantic_mono | Dart Package


使い方

細かい手順はReadmeに書いてあるため、概要だけ

テンプレートの選択

新規リポジトリを作成するとき、Repository Templateを指定します

f:id:aoshima214:20211231012630p:plain

新規アプリ名に書き換え

applicationIdやパッケージ名を、新規アプリのものに書き換えます

f:id:aoshima214:20211231012649p:plain

Firebaseの設定

google-services.json, GoogleService-Infoを配置します

f:id:aoshima214:20211231012706p:plain

参考記事

blog.dalt.me

zenn.dev

docs.flutter.dev

iOSの証明書周りのことを理解したい

来年はもっとFlutterで個人開発したいな〜と思ってます。

その前に、何となくでやっていたiOSのよくわかってない部分を解明します。
証明書については既に色々な方の解説記事がありますが、自分でまとめないとわからないなと思ったので書きます。

参考記事

qiita.com

この方の記事が1番わかりやすいと思います。
この記事の画像を基にして、具体的な手順を補足していきます。

CSRを作成する (①〜②)

CSR = Certificate Signing Request (証明書署名要求)

キーチェーンアクセスを起動して、画像の場所を押すと
証明書アシスタント画面が出るので、メールアドレス等を打ち込んで
「CertificateSigningRequest.certSigningRequest」ファイルを生成します。

f:id:aoshima214:20211209221652p:plain:w400

Certificateを作成する (③)

Apple Developper にアクセスします
https://developer.apple.com/account/resources/certificates/list

Certificates の + ボタンを押すと、以下の画面が表示されます

f:id:aoshima214:20211209225950p:plain

Apple ~~ と、iOS App ~~ があります。

Apple ~~ はXcode11以降で使用します、とあるので、
現在はApple~~ の方を選択すればよさそうです。

その後CSRファイルを選択する画面に移るので、先ほど作ったファイルを選択して、 Certificateを発行します。

Certificateをキーチェーンに登録する (④)

③で作成したCertificateをダウンロードして、ダブルクリックでキーチェーンに登録します。

Identifiers に App IDを登録する (⑤)

f:id:aoshima214:20211210221015p:plain

  • Description = 管理画面上の名前
  • Bundle ID = XcodeプロジェクトのBundleID
ExplicitとWildcard
  • Explict = XcodeプロジェクトのBundleIDと完全一致したもの
  • Wildcard = Bundle IDの一部をワイルドカードにして、複数のアプリで共通して使用できるようにする

Explictでないと、特定の機能が使えない
(iCloud、プッシュ通知、In-App Purchase、Game Center 等)
開発専用など、 いちいちAppIDを作りたくない場合はWildcardを使う?

Devicesに、テスト用の端末を登録する (⑥)

ここに登録した端末は以下のことができます

  • 実機ビルド
  • Ad Hoc配布の .ipaファイルのインストール

Provisioning Profileの生成 (⑦)

f:id:aoshima214:20211210234637p:plain

使いそうなやつに印をつけました。

上から 開発用、社内配布用、リリース用 みたいに使い分ける感じでしょうか。

この後、前の工程で作成したApp ID、Certificate、Devicesを選択して Provisioning Profileを生成します。

Xcodeでビルド設定をする (⑧〜⑨)

f:id:aoshima214:20211211000830p:plain:w500

ビルド、アーカイブ (⑩)

Product -> Archive

複数台のMacで開発する場合に必要な作業

プロビジョニングファイル内の証明書内の公開鍵に対応する秘密鍵が端末にないとビルドができません。
(上記①〜③の作業を行なっていない端末では、⑨のビルド設定をしただけではビルドできない)

.p12形式のファイルを他の端末からもらう必要があります。

Xcode -> Preferences -> Accounts -> Manage Certificate で 証明書を右クリックしてexport したものを、別の端末に渡します。

まだ知らないこと

  • Automatically manage signingの挙動
  • Provisioning Profileなしで実機ビルドできるやつは何

RecyclerViewでオセロをつくる

なんか楽しいことしてえ... ってなったのでオセロをつくりました

完成品

f:id:aoshima214:20211129233714g:plain:w300

つくり方

オセロの条件判定とかの部分は、ネットにもたくさん記事がありますし、そこまで複雑ではないので根性で書けます。

それよりも、UIをどうするかが問題になります。

  • 盤面をどう描画するか
  • タップされた位置をどう取得するか
  • 置かれた石・ひっくり返った石がどれか、わかるようにしたい

これらはRecyclerViewのGridLayoutManagerと、ListAdapterの差分更新の仕組みを使うことで簡単に解決できます。


盤面とセルのレイアウトを作る

背景を黒くしたRecyclerView
f:id:aoshima214:20211129224643p:plain:w250

1マス分のセルのレイアウト
f:id:aoshima214:20211129225659p:plain:w50 f:id:aoshima214:20211129225104p:plain:w50

RecyclerViewにGridLayoutManagerをセット

 // BOARD_SIZE = 8
recyclerView.layoutManager = GridLayoutManager(this, BOARD_SIZE)

これらを組み合わせると↓のようになります

f:id:aoshima214:20211129230521p:plain:w300


盤面の状態管理とタップされた位置の取得

オセロの盤面の状態は二次元リストとして保持したいです。
RecyclerViewには、これを単一のリストに変換して渡します。

このとき、リストに詰めるのは以下のようなデータクラスにします。

data class Cell(
    val vertical :Int,
    val horizontal : Int,
    var stone: Stone
)

enum class Stone {
    BLACK,
    WHITE,
    NONE
}

Cell自体に自分の位置情報を持たせると、Adapterからタップされた座標を得るのが簡単になる上、 今後色々な条件判定を書くときに便利です。


置かれた石、ひっくり返った石だけアニメーションさせる

ListAdapter & DiffUtil を使います

developer.android.com

class CellAdapter() : 
    ListAdapter<Cell, CellAdapter.ViewHolder>(CellAdapterDiffCallback()) {
        ........
}


class CellAdapterDiffCallback : DiffUtil.ItemCallback<Cell>() {
    // アイテム自体が同じか
    override fun areItemsTheSame(oldItem: Cell, newItem: Cell): Boolean {
        return oldItem.vertical == newItem.vertical && 
                oldItem.horizontal == newItem.horizontal
    }
    // アイテムの内容が同じか
    override fun areContentsTheSame(oldItem: Cell, newItem: Cell): Boolean {
        return oldItem == newItem
    }
}

adapterにsubmitList() すると、すでに表示されているリストとの差分だけ更新 & アニメーションされます。

加えて、postDelayed() を処理の合間に挟んでゲームスピードを遅くするといい感じになります。

参考にしたもの

Kotlinの練習にオセロ作って遊んだ - みんからきりまで
8方向の判定方法について参考にさせていただきました。

オセロ(リバーシ)の作り方(アルゴリズム) ~石の位置による評価~
オセロのAIについて参考にさせていただきました

感想

それなりの時間で作れてプログラミングっぽさも味わえて楽しいのでおすすめです

今回のコードは以下にあります

github.com

Androidのテストを書くまでの手順まとめ

仕事ではほとんどテストコードを書いたことがないわたしです。

流石に全く知らないのはまずいよな、と思って最近リリースした個人開発アプリにテストコードをちょっと追加してみました。
書き始めるまでの手順がややこしくて混乱してきたので、メモとして残します。

はじめに

Jetpack(AAC),Coroutine,Dagger Hiltを使って作成されたアプリを前提としています

参考にしたもの

Codelab

developer.android.com AndroidのテストについてのCodelabです。
なぜか05.2でリンクが途切れていますが、探すと05.3まであります

公式のリポジトリ

github.com

上記のCodelabはこのリポジトリのコードをベースに説明されています。
ここからテスト用のUtilsファイルとかをコピーして使ったりもします。


関数のテスト

ライブラリの追加

お好みで以下のライブラリを追加します

testImplementation "org.hamcrest:hamcrest-all:1.3"

hamcrestはテストコードを読みやすく記述するためのアサーションライブラリです。
以下のように記述できます。

// ふつう
assertEquals(result, 0f)

// hamcrest
assertThat(result, `is`(0f))

データベース(Room)のテスト

ここからのテストはAndroidフレームワークに依存するため、テスト用のライブラリを追加したり、色々と準備が必要になります。

ライブラリの追加

以下のライブラリを追加します

testImplementation "androidx.test.ext:junit-ktx:1.1.3"

// ユニットテストでcontextを使うために必要
testImplementation "androidx.test:core-ktx:1.4.0" 

// ローカルテストでAndroid環境をシミュレートする
testImplementation "org.robolectric:robolectric:4.6.1"

 // テストでCoroutineを実行する (RunBlockingTest)
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2"

 // AACを同期的に実行する (InstantTaskExecutorRule)
testImplementation "androidx.arch.core:core-testing:2.1.0"

build.gradleの編集

build.gradle(app) に以下を追加します

android {
    ...
    testOptions {
        // Unit Test でRobolectricを使うために必要
        unitTests.includeAndroidResources = true
    }
}

テストを書く

@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class RadarChartDaoTest {

    private lateinit var database: RadarChartDatabase

    // Executes each task synchronously using Architecture Components.
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

    @Before
    fun initDb() {
        database = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            RadarChartDatabase::class.java
        ).allowMainThreadQueries()
            .setQueryExecutor(Executors.newSingleThreadExecutor()).build()
    }

    @After
    fun closeDb() = database.close()

    @Test
    fun insertGroupAndGetById() = runBlocking {
        // GIVEN - Groupをインサート
        val group = ChartGroup(
            title = "test",
            color = 12345,
            maximumValue = 200
        )
        database.radarChartDao().insertGroup(group)

        // WHEN - Groupをデータベースから取得
        val loaded = database.radarChartDao().getGroupById(group.id)

        // THEN - グループが一つだけあり、期待値が含まれること
        assertThat(loaded, notNullValue())
        assertThat(loaded.id, `is`(group.id))
        assertThat(loaded.color, `is`(group.color))
        assertThat(loaded.maximumValue, `is`(200))
    }
}

参考リンク

AndroidXテストを使用したViewModelテストの設定
7. Task: Testing Room


リポジトリのテスト

MainCoroutineRuleをコピーする

公式のリポジトリから、MainCoroutineRule.kt をコピーします。 https://github.com/android/architecture-samples

@ExperimentalCoroutinesApi
class MainCoroutineRule : TestWatcher(), TestCoroutineScope by TestCoroutineScope() {

    override fun starting(description: Description?) {
        super.starting(description)
        Dispatchers.setMain(this.coroutineContext[ContinuationInterceptor] as CoroutineDispatcher)
    }

    override fun finished(description: Description?) {
        super.finished(description)
        Dispatchers.resetMain()
    }
}

これが必要な理由は以下に書いてあります。
Advanced Android in Kotlin 05.3: Testing Coroutines and Jetpack integrations

【抜粋】
- viewModelScopeは、Dispatchers.Mainコルーチンディスパッチャを使用する
- Dispatchers.Mainは実際のアプリケーションの実行ループの仕組みを使っている
- ローカルテストでは実際のアプリケーションを実行する訳ではないのでTestCoroutineDispatcherを使う
- TestCoroutineDispatcher を設定したり削除したりするコードを各テストクラスにコピー&ペーストするのではなく、 JUnitのカスタムルールを作ることでこのような定型的なコードを避けることができます。

偽のデータベースを作る

先ほどテストしたRoomの偽の実装を作成します。
今回は本物の実装が既に完成しているので、それを使ってテストを書くことも可能でしたが.

  • テスト対象のクラス(リポジトリ)のコードのみをテストするべきではないか
  • このあとのViewModelとかFragmentのテストでもFakeを使いたい

などの理由から、データベースではなくメモリに値を保存するFakeを作成しました。

class FakeDao : RadarChartDao {

    private val groupList = mutableListOf<ChartGroup>()
    private val groupLabelList = mutableListOf<ChartGroupLabel>()
    private val chartList = mutableListOf<MyChart>()
    private val chartValues = mutableListOf<ChartValue>()

    override suspend fun insertGroup(group: ChartGroup) {
        groupList.add(group)
    }

    override suspend fun getGroupById(groupId: String): ChartGroup {
        return groupList.first { it.id == groupId }
    }
~~~~~
}

テストを書く

@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class RadarChartRepositoryTest {

    private val group1 = ChartGroup(title = "Group1", color = 1234, maximumValue = 100)
    private val groupLabelTextList = mutableListOf("Label1", "Label2", "Label3", "Label4", "Label5")

    private lateinit var repository: RadarChartRepository

    private val mockPreferences = mockk<RadarChartPreferences>() 

    // Set the main coroutines dispatcher for unit testing.
    @ExperimentalCoroutinesApi
    @get:Rule
    var mainCoroutineRule = MainCoroutineRule()

    @Before
    fun createRepository() {
        repository = RadarChartRepositoryImpl(
            radarChartDao = FakeDao(),
            ioDispatcher = Dispatchers.Main,
            preferences = mockPreferences
        )
    }

    @Test
    fun saveGroupAndGetById() = mainCoroutineRule.runBlockingTest {
        // GIVEN
        repository.saveGroup(group1, groupLabelTextList, null)

        // WHEN
        val loaded = repository.getGroupById(group1.id)

        // THEN
        assertThat(loaded, notNullValue())
        assertThat(loaded.group.id, `is`(group1.id))
        assertThat(loaded.labelList.size, `is`(5))
    }

参考リンク

Advanced Android in Kotlin 05.3: Testing Coroutines and Jetpack integrations
Advanced Android in Kotlin 05.2: Introduction to Test Doubles and Dependency Injection


ViewModelのテスト

LiveDataTestUtilをコピーする

公式のリポジトリから、LiveDataTestUtil (LiveData.getOrAwaitValue) をコピーします。
https://github.com/android/architecture-samples

【抜粋】
 - ViewModelのユニットテストでは、LiveDataを監視するLifeCycleOwner(Fragmentなど)がない
 - よってLiceCycleOwnerなしでオブザーバーを登録できるobserveForever()を使う
 - observeForeverを使う場合は、テストの最後にremoveObserverを呼び出す必要がある
 - これらを簡単に記述するためのLiveData.getOrAwaitValue() という拡張関数を用意した

偽のリポジトリを作成する

先ほどと同じ理由なので割愛

テストを書く

@ExperimentalCoroutinesApi
class GroupListViewModelTest {

    // テスト対象クラス
    private lateinit var groupListViewModel: GroupListViewModel

    // Set the main coroutines dispatcher for unit testing.
    @ExperimentalCoroutinesApi
    @get:Rule
    var mainCoroutineRule = MainCoroutineRule()

    // Executes each task synchronously using Architecture Components.
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

    @Before
    fun setupViewModel() = runBlocking {
        val fakeRepository = FakeRepository()
        fakeRepository.setupDefaultData()
        groupListViewModel = GroupListViewModel(fakeRepository)
    }

    @Test
    fun onViewCreatedAndGetGroupList() {
        // WHEN
        groupListViewModel.onViewCreated()

        // THEN
        groupListViewModel.groupList.observeForTesting {
            assertThat(groupListViewModel.groupList.getOrAwaitValue().size, `is`(2))
        }
    }

Fragmentのテスト

ここからはandroidTestフォルダにテスト用のファイルを追加していきます。

ライブラリの追加

以下のライブラリを追加します。

androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
debugImplementation "androidx.fragment:fragment-testing:1.3.6"

なぜandroidTestImplementationではなくdebugImplementationなのかについては以下で説明されています
https://issuetracker.google.com/issues/128612536

Dagger Hiltを使う場合

アプリでDagger Hiltを使っている場合は以下の手順を続けます

ライブラリを追加
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
Instrumented testsでHiltを使用するためのテストランナーを作成
// A custom runner to set up the instrumented application class for tests.
class CustomTestRunner : AndroidJUnitRunner() {

    override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
        return super.newApplication(cl, HiltTestApplication::class.java.name, context)
    }
}

build.gradle(app)に以下を追加

android {
    defaultConfig {
        // Replace com.example.android.dagger with your class path.
        testInstrumentationRunner "com.example.android.dagger.CustomTestRunner"
    }
}
launchFragmentInHiltContainerをコピーする

ここまでの設定でActivityのlaunchはできるようになりました。
しかしandroidx.fragment:fragment-testing ライブラリの launchFragmentInContainerは、Hiltを使ったアプリで使うことができません。

以下の手順でHiltでもlaunchFragmentInContainerを使えるようにします。

  • src/ の配下にdebugディレクトリを作成する
  • HiltTestActivityを追加する(同時にAndroidManifest.xmlも生成される)
@AndroidEntryPoint
class HiltTestActivity : AppCompatActivity()
inline fun <reified T : Fragment> launchFragmentInHiltContainer(
    fragmentArgs: Bundle? = null,
    @StyleRes themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme,
    crossinline action: Fragment.() -> Unit = {}
) {
    val startActivityIntent = Intent.makeMainActivity(
        ComponentName(
            ApplicationProvider.getApplicationContext(),
            HiltTestActivity::class.java
        )
    ).putExtra(
        "androidx.fragment.app.testing.FragmentScenario.EmptyFragmentActivity.THEME_EXTRAS_BUNDLE_KEY",
        themeResId
    )

    ActivityScenario.launch<HiltTestActivity>(startActivityIntent).onActivity { activity ->
        val fragment: Fragment = activity.supportFragmentManager.fragmentFactory.instantiate(
            Preconditions.checkNotNull(T::class.java.classLoader),
            T::class.java.name
        )
        fragment.arguments = fragmentArgs
        activity.supportFragmentManager
            .beginTransaction()
            .add(android.R.id.content, fragment, "")
            .commitNow()

        fragment.action()
    }
}
テストを書く
@MediumTest
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class SettingFragmentTest {

    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    @Before
    fun init() {
        hiltRule.inject()
    }

    @Test
    fun displayItem() {
        // WHEN
        launchFragmentInHiltContainer<SettingFragment>()

        // THEN
        onView(withId(R.id.privacy_policy)).check(matches(isDisplayed()))
    }
}

参考リンク

Hilt testing guide  |  Android Developers

感想

準備が大変

今回書いたコードは以下にあります。

github.com

RoomのDaoのユニットテストが終了しない

最近自分のアプリにユニットテストを少しずつ書いています。

テスト実行ボタンを押した後どれだけ待っても終了しないテストケースがあり、その解決方法がわかったので書きます。

原因

@Transaction 付きのメソッドをrunBlockingTest{} 内で呼び出すとテストが終了しなくなる

解決策

SingleThreadExecutorをセット

return Room
       .inMemoryDatabaseBuilder(context, MyRoomDatabase::class.java)
       .setTransactionExecutor(Executors.newSingleThreadExecutor())
       .build()

runBlockingTest -> runBlocking に変更

@Test
fun moveItem() = runBlocking {
    transactionFunction()
}

参考

Android instrumentation test doesn't run to end when using Room @Transaction function

Flutterでメモ帳アプリを作ったときの話

もうだいぶ前ですが、今年の初めにFlutterで簡単なアプリを作りました。

今更ですが、今回はそのときのことについてまとめたいと思います。

作ったアプリ

f:id:aoshima214:20211028194634p:plain:w500

iOS

‎「Color Folder - カラフルなメモ帳アプリ」をApp Storeで

Android

Color Folder - カラフルなメモ帳アプリ - Apps on Google Play

書いたコード

github.com

主な機能

色分けしたフォルダーでメモを管理します。

f:id:aoshima214:20211028194917g:plain:w250

僕はあんまりスマホでたくさんメモとるタイプではなく、文字をちょっと書ければ何でもいいと思ってます。
機能はシンプルでいいから、コントラストがはっきりした、目が泳がないメモ帳を作ろう!というのがコンセプトです。

インストール数はお察しですが、個人的にはわりと気に入って使ってます笑

作ろうと思ったきっかけ

  • 2021年に入って、そろそろFlutterのこと全く知らないのはまずくないか、という気になる
  • とりあえず簡単なアプリ作ってみるか!
  • アプリの題材何も思いつかないしメモ帳でいいか!

開発期間

2021/01/24 〜 03/02
(初コミット から リリースボタンを押すまで)

大体100時間くらいかかりました。

学習方法

Youtube

www.youtube.com

KBOY氏が運営するFlutter大学の動画です。
わかりやすい!

Flutter documentation

flutter.dev

整いすぎている公式ドキュメント

Widget of the Week

www.youtube.com

最初はぼーっと流し見して、どんなものがあるか何となく頭に入れておきました。
こういうレイアウトはどうやって組むんだ?と詰まったら、関係ありそうな動画を何回か見直しました。

使ったライブラリ

名前 説明
firebase_crashlytics クラッシュレポートの収集
sqflite ローカルデータベース
flutter_hooks 状態管理
hooks_riverpod 状態管理
fluttertoast トーストでメッセージを表示
短いエラーメッセージを表示するのに使用
flutter_colorpicker 高機能なカラーピッカー
アプリのテーマカラーを選択するのに使用
flex_color_picker シンプルなUIのカラーピッカー
フォルダーの色を選択するのに使用
shared_preferences ローカルファイルにデータを保存
設定内容を保存するのに使用
dynamic_theme themeを動的に変更する
アプリのテーマカラーを変更するのに使用
launch_review アプリからストアのレビューページに遷移させる
webview_flutter FlutterでWebViewを使う
プライバシーポリシーのWebページを表示するのに使用

反省点

状態管理のライブラリについて

結論から言うと、状態管理のライブラリの理解が浅くてめちゃめちゃになりました。

アプリを作り始める前、Flutterのアーキテクチャの解説記事を何個か読んでみて、

  • どうやらFlutterは状態管理のライブラリを使うのが一般的らしい
  • Providerとか、それを改良したRiverpodなるものがあるらしい
  • 使うと何がいいのかはよくわからんがとりあえず使ってみるか

という流れでとりあえずRiverpodを入れました。

さっぱり分からない

なにがわからないかわからない

  • ここの意味が分からないな...調べるか...この記事はProvider? Riverpod?
  • Hooksってなんだよ
  • StatefullWidgetに比べて何がいいのか

根性で実装した結果

f:id:aoshima214:20211028225202g:plain
↑項目をいじると画面全体が再描画される設定画面の爆誕

どうすればよかったか

  • 一度に色々やろうとしない
  • 最初は「StatefullWidgetでできることを覚える」だけでもよかった
  • 使うなら、ある程度インプットに時間をかける

あれから日本語でRiverpod等の体系的な解説記事を書いてくださる方も増えたので、次はそれを熟読して挑みます!

次やりたいこと

CI/CDとか

楽しそう
codemagic.io

FireStore

次はバックエンドもあるアプリ作りたい firebase.google.com

感想

2022年はAndroid、Flutter、Firebaseの3本立てで頑張ります