Author: Catzie

  • Run command automatically after updating a file on Mac

    I get a kick out of automating things! Especially those seemingly easy extra steps that we do numerous times a day, everyday.

    There’s this configuration file that I frequently have to adb push into my test Android devices. And the cycle of editing the file, saving it, and then going to the terminal to run the push command (sometimes having to press the up ↑ key a few times) , is repeated as many times as needed.

    Last week, while rapidly becoming exhausted from the extreme summer heat, I was becoming frustrated at the bug I was trying to fix. Having to repeat the edit-save-push cycle on the file a countless number of times, along with the discomfort from sweaty armpits, wasn’t helpful in fixing the bug.

    I wondered, how do I automatically push this configuration file when unix detects a change in the file after editing and saving?

    What I did was rely on “entr”.

    I first installed it on my mac like so: brew install entr

    And then ran below command in my terminal:

    ls /Users/catz/Documents/MyConfigFile.json | | entr adb -e push /Users/catz/Documents/MyConfigFile.json /sdcard/Android/data/com.example.app/files/config/MyConfigFile.json

    /Users/catz/Documents/MyConfigFile.json is the configuration file I frequently edit and push.

    /sdcard/Android/data/com.example.app/files/config/MyConfigFile.json is the expected location of the file in my test Android device. The app reads the configuration from this location.

    Now, with this setup, I simply have to hit Save after editing the configuration file, and I just watch it automatically get pushed to my test Android device. 😎 My younger self probably would have tried to code her own script for automation, but my old, cranky present self knows better than waste time like that. Heh.

    Automating little things, such as having to push a changed file each and every time it’s edited, can have a big impact in one’s workflow. Streamlining mine this way allows me to use my little energy on what really matters.

    So, how did the bug squashing go?

    A quick background on the bug: StateFlow.update was not updating a value inside a list of data class model. I thought that a deep copy was what I needed, but it turned out that the data class contained an override function of equals and it was lacking a necessary logic to return the correct Boolean value. StateFlow.compareAndSet, which is called by StateFlow.update behind the scenes, does not proceed with updating the value, because the old and new value are seen as “equal” due to the overridden equals function not returning the correct result.

    I didn’t notice the faulty equals logic until I regained my calm focus on that hot workday — all thanks to a quick cool shower, and of course, the kick I got from setting up my command to automatically run after updating the configuration file~

    And yes, remember to try to feel comfortable as you do problem solving tasks! I think that feeling comfortable helps us keep a clear head, which consequently helps us solve problems more effectively.

  • KoleilYutong.. what? And other cryptic credit card charges from South Korea, Japan, etc.

    I’ve recently been to South Korea and had a great time. Learning how to read Hangeul helped a lot, even though I don’t know the meaning of most things that I read!

    One of the things I liked about traveling to South Korea was that many establishments accepted credit card for payment. When traveling abroad, I prefer managing my finances with the help of a credit card, so most of my expenses in Korea were charged to it.

    The only challenge was figuring out which establishment charged which bill entry, because the labels can be cryptic.

    It took some effort, but I’ve finished deciphering where each credit card charge came from.

    I thought it would be nice to share them in case anyone faces the same challenge. Though I don’t know if this blog entry will still show up on Google search results. Ahh, fall of blogs’ popularity… (And years of me neglecting this blog :-p)

    Credit card charge labels from South Korea:

    Anyway, here are the credit card charges from South Korean establishments that I was able to decipher:

    “KOLEILYUTONG(JOO)ÝR.FJ Seoul KR”: This was from an Etude House cosmetics vending machine, which was located in a Korail subway station. I had already flown back home when I realized that we got double-charged by the vending machine that only released one item. T_T Anyway, I have read elsewhere, that someone was charged “KOLEILYUTONG(JOO)NAM BOOSAN REF# MISC/SPECIALTY R” for baggage deposit at a train station. So I think that if you see any “KOLEILYUTONG” charge from your credit card, it could be a retailer that you transacted with in a Korail subway station. Fun fact: If you search for “코레일유통” (KorailYutong) you may stumble upon the “Korail Retail” website, which is currently at korailretail.com as I write this post!

    “SSIJEOIIOLLIBEUNETEUUE SEOUL KR”: Olive Young

    “AK PLAZA SOOUEONJEOM Suwon KR”: SPAO in AK Plaza, Suwon City

    “OLMASHUKEUSHUTORI 4HOJ SEOUL KR”: All Mask Story in Myeong-dong, Seoul City

    “7-ELEVEN Suwon KR”: Self-explanatory 😛

    “INISEUPEULI MYEONGDONG Seoul KR”: Innisfree in Myeong-dong, Seoul City

    “E-MART 24 SUWON STATI SUWON-SI P KR”: E-Mart convenience store in Suwon Station

    “LODDEOIJIALENJEOILINEO SUWON-SI KR”: ANGELINUS, formerly known as Angel-in-us Coffee, in Suwon Station

    “LIGIMBABAKSOOUEONJEOM SUWON-SI P KR”: Lee Gimbap, in Suwon Station

    “SSIYU(CU) JINJUPLIMIUM” / “SSIYU(CU) INKYEPLIMIUM“: CU (Convenience Store) Jinju Premium branch / Ingye Premium branch

    2025 update with some more credit card charge labels from Korea:

    • “AK PLAZA Suwon KR”: This is a purchase from the AK Plaza Mall, located in Suwon City, South Korea.
    • “CJ OLIVE YOUNG GANGNAM DAEJEON KR”: one of the Olive Young (a health & beauty store) branches in Gangnam.
    • “CJ OLIVE YOUNG GANGNAM SEOUL KR”: one of the Olive Young (a health & beauty store) branches in Gangnam.
    • “CO.,LTD ART BOX SUWON SUWON-SI P KR”: This is a purchase from Art Box, a “lifestyle and stationery” store in Suwon City, South Korea
    • “SSIYU(CU) INCHUNGONGHA INCHEON KR”: SSIYU is CU, a popular CVS (convenience store) in South Korea. This one is specifically a branch at Incheon Airport.

    New! Credit card charge labels from Japan:

    • “Mujirushiryohin” is from MUJI!
    • “Junkudoushoten Ikebuku”: A branch of Junkudo Shoten bookstore in Ikebukuro
    • “Toraberazufuakutori”: This is Traveler’s Factory, a shop that sells stationery such as Traveler’s Notebooks and other retro-vibe tools!
      • “Toraberazufuakutori” is from the NAKAMEGURO Traveler’s Factory branch.
      • “Jr East Shopping Cente”: Traveler’s Factory STATION branch in Tokyo Station. I think the charge is labeled as such because the shop’s operation is tied to JR East (a railway company).
      • “Travelers Factory – Chiba” – Traveler’s Factory AIRPORT branch at Narita Airport

    That’s all folks! :3

    On a side note, I had to fix this WordPress site because an update broke it! Good thing that my host’s support agent was able to point me to a way to restore my site to a previous state. Phew. I don’t like WordPress anymore but I can’t be bothered to plan and actually do the moving to another platform. Maybe someday, on a day, far faaaar away. Or, in a parallel universe.

  • Make a RecyclerView expand its height when new item is added //Android Dev Diary

    It seems that simply using android:layout_height="wrap_content" on your RecyclerView won’t make its height automatically expand when new items are added in it.

    (more…)
  • How to start using view binding //Android Kotlin Dev Diary

    First, open your app/build.gradle file

    Inside the android{} block of app/build.gradle, add the following:

    buildFeatures { viewBinding = true }

    Sync project with gradle files.

    In your activity or fragment, define the following class-wide property:

    private lateinit var binding: FragmentorActivityNameBinding

    Replace the FragmentorActivityNameBinding above with the appropriate binding class name. Tip: The binding class name is based on the XML file’s name. For example, if your XML file name is activity_main.xml, the binding class name should be ActivityMainBinding. Or, if your file name is fragment_main.xml, the binding class name should be FragmentMainBinding.

    In your activity, the onCreate function should look like this:

    override fun onCreate(savedInstanceState: Bundle?) {
    
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
    }

    Otherwise, if using view binding in a fragment, your onCreateView should look like this:

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
    
        binding = FragmentMainBinding.inflate(inflater, container, false)
        binding.lifecycleOwner = viewLifecycleOwner
        binding.viewModel = mainViewModel
        return binding.root
    }
  • Disable animations programmatically during instrumented tests // Android Dev Diary

    When running an instrumented Android test, or “androidTest”, and you encounter the following error:

    androidx.test.espresso.PerformException: Error performing 'scroll to' on view 'Animations or transitions are enabled on the target device.

    It means that you need to disable animations for it to successfully run the test.

    But, it can take a lot of clicks on the emulator/device screen to get to the developer settings to turn off animations!

    So, for a quicker way to disable animations in the Android device during development, just enter the following commands in your Android Studio terminal:

    adb shell settings put global window_animation_scale 0.0
    adb shell settings put global transition_animation_scale 0.0
    adb shell settings put global animator_duration_scale 0.0
  • Exception: No signature of method android() is applicable for argument types, Build Gradle // Dev Diary

    Today I wanted to make a data class a Parcelable so that I could pass it as an argument in Navigation component.

    Parcelables, though better in performance than Serializables, take more time to implement! To help us out, there’s a “kotlin-parcelize” plugin that can generate the boilerplate code for us.

    There were issues though.

    After applying the kotlin-parcelize plugin, I was getting the error:

    'kotlin-parcelize' can't be applied together with 'kotlin-android-extensions'

    So kotlin-parcelize is like a “successor” to the now deprecated 'kotlin-android-extensions' that I’m still using. So I replaced 'kotlin-android-extensions' with ‘kotlin-parcelize’.

    I got another error after attempting to sync with my Gradle files:

    org.gradle.api.GradleScriptException: A problem occurred evaluating project ':app'
    [...]
    Caused by: groovy.lang.MissingMethodException: No signature of method: build_54gmhuqjm83le8li4fspjsxlu.android() is applicable for argument types: (build_54gmhuqjm83le8li4fspjsxlu$_run_closure1) values: [build_54gmhuqjm83le8li4fspjsxlu$_run_closure1@3c374eea]
    	at build_54gmhuqjm83le8li4fspjsxlu.run([...]\app\build.gradle:11)
    

    The solution was to remove the following code from my android{} block:

    androidExtensions {
        experimental = true
    }

    After removing that, Gradle synced successfull!

    References:
    Parcelable implementation generator
    How to use @Parcelize now that kotlin-android-extensions is being deprecated?

  • Two-way data binding with StateFlow & BindingAdapters //Dev Diary

    I created BindingAdapters for an EditText like so:

    @BindingAdapter("android:text")
    fun setText(editText: EditText, inputAmount: MutableStateFlow<Double>) {
        if (editText.text.toString() != inputAmount.value.toString()) {
            editText.setText(inputAmount.value.toString())
            editText.setSelection(editText.text.length)
        }
    }
    
    @InverseBindingAdapter(attribute = "android:text")
    fun getText(editText: EditText): Double {
        if (editText.text.isEmpty()) return 0.0
        return editText.text.toString().toDouble()
    }
    
    

    My XML layout element looks like this:

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/tiet_input_amount"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:hint="@string/input_hint_amount"
                    android:inputType="numberDecimal"
                    android:text="@={viewModel._inputAmount}" />

    And in my ViewModel, the inputAmount is set up this way:

        val _inputAmount: MutableStateFlow<Double> = MutableStateFlow(0.0)
        val inputAmount: StateFlow<Double> = _inputAmount