Tag: Kotlin

  • Android: Broadcast sent, receiver registered, but receiver doesn’t receive broadcast

    I should blog more often about things I learn about.

    In a native Android app written in Kotlin, I have a LifecycleService class with below code to send a broadcast:

        val eventIntent = Intent(CUSTOM_EVENT_ACTION_NAME).apply {
        flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
        }
        applicationContext.sendBroadcast(eventIntent)
        Log.d("AppTag", "Broadcast sent!")



    Based on the logcat, the broadcast is being sent.

    However, below BroadcastReceiver in an Activity doesn’t receive the broadcast, even while it is visible:

    
    private val customEventReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            Log.d("AppTag", "Custom event receiver triggered")
            if (intent?.action == GlobalConstants.CUSTOM_EVENT_ACTION_NAME) {
                Log.d("AppTag", "Confirmed action match, proceeding with task.")
                performSpecificTask()
            }
        }
    }

    The above BroadcastReceiver is registered from the Activity’s onCreate function, like so:

    val filter = IntentFilter(GlobalConstants.CUSTOM_EVENT_ACTION_NAME)
    ContextCompat.registerReceiver(
        this,
        customEventReceiver,
        filter,
        ContextCompat.RECEIVER_NOT_EXPORTED 
    )
    
    

    Attempt #1: LocalBroadcastManager – it worked, but…

    After browsing the web for potential solutions I came across LocalBroadcastManager. The broadcast was successfully received by the receiver after I changed the sendBroadcast’s applicationContext and the registerBroadcast’s ContextCompat into LocalBroadcastManager.getInstance(Context).

    That is, LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(eventIntent) and LocalBroadcastManager.getInstance(this).registerReceiver(customEventReceiver, filter).

    The problem with this approach is that LocalBroadcastManager has been deprecated:

    Important changes since 1.0.0

    androidx.localbroadcastmanager has been fully deprecated. There will be no further releases of this library. Developers should replace usages of LocalBroadcastManager with other implementations of the observable pattern. Depending on the use case, suitable options may be LiveData or reactive streams.

    https://developer.android.com/jetpack/androidx/releases/localbroadcastmanager

    Ideally, the solution should not be a deprecated one.

    As to why LocalBroadcastManager made my receiver work, I think it’s because it “bridged” 2 components that weren’t really supposed to be able to communicate. Because from the Android docs:

    LocalBroadcastManager is an application-wide event bus and embraces layer violations in your app: any component may listen events from any other.

    Why it works, and why it’s been deprecated, apparently have the same explanation. 😅

    Attempt #2: Use applicationContext on the sender and receiver — Nope!

    I reverted my code to the original, and just made sure they all used applicationContext, matching the context used in the service’s broadcast sender applicationContext.sendBroadcast(eventIntent).

    So that my registration became:

    val filter = IntentFilter(GlobalConstants.CUSTOM_EVENT_ACTION_NAME)
    ContextCompat.registerReceiver(
        applicationContext,
        customEventReceiver,
        filter,
        ContextCompat.RECEIVER_NOT_EXPORTED 
    )
    
    
    And the code to unregister became:
    
    applicationContext.unregisterReceiver(customEventReceiver)

    Hmm… Nope! that didn’t let my receiver get the broadcast.

    Attempt #3: Make it an explicit broadcast — YES, IT WORKS!

    I reverted my code again to try another approach, which is to make the broadcast an explicit one.

    So, in my service, I simply set the package name as shown in code below:

    val eventIntent = Intent(CUSTOM_EVENT_ACTION_NAME).apply {
        setPackage(applicationContext.packageName)
        //...

    And with this 1 line changed in code, my BroadcastReceiver now receives the broadcast! Plus, there’s no deprecated class used here so we prevented 1 new technical debt. :3

    Yeah, I think I’ll stick to the 3rd attempt’s approach!

    Why does this approach work, though? It’s because starting from Android API level 26, there’s been restriction on implicit broadcasts.

    As part of the Android 8.0 (API level 26) background execution limits, apps that target the API level 26 or higher can’t register broadcast receivers for implicit broadcasts in their manifest unless the broadcast is sent specifically to them. However, several broadcasts are exempted from these limitations. 

    https://developer.android.com/develop/background-work/background-tasks/broadcasts/broadcast-exceptions

    My original code’s broadcast Intent was an implicit one, but specifying a package name converted it into an explicit one, consequently lifting the limitation applied to implicit broadcasts!

  • 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.

  • 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
  • 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
  • Fix coroutine error: Cannot access database on the main thread // Android Dev Diary

    As I was running a test for a ViewModel function, I encountered the following error:

    [...] Main Thread @coroutine java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

    I think it may be caused by the viewModelScope from viewModelScope.launch {} being used by the running test, which may not be accessible during unit tests.

    What I did to fix this error on my unit test was to enable main thread operations in my in-memory Room database test instance. I simply added .allowMainThreadQueries() while building the database instance like so:

    db = Room.inMemoryDatabaseBuilder(
        context, MyRoomDatabase::class.java)
        .allowMainThreadQueries()
        .build()

    Is that the best way to do it? If you got a better suggestion, please comment! I’d love to hear about it.

  • Two NavHosts with their own NavControllers and NavGraphs (Navigation Component) //Android Kotlin Dev Diary

    I am trying to make a single-activity app because I’d like to easily share data across screen using, ideally one, or as few as possible ViewModels.

    NavGraph 1

    Host: Layout file of MainActivity

    Destination(s): CreateLogFragment

    NavGraph 2

    Host: Layout file of CreateLogFragment

    Destination(s): CreateLogFoodFragment, CreateLogNotesFragment

    (CreateLogFragment has a BottomNavigationBar that switches between CreateLogFoodFragment and CreateLogNotesFragment)

    The problem was that NavGraph 2 was not getting the correct NavHost and NavController in the CreateLogFragment class.

    And the solution is to retrieve the NavHost in CreateLogFragment like so:

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

        val navHostFragment = childFragmentManager.findFragmentById(R.id.nav_host_create_log) as NavHostFragment
        val navController = navHostFragment.navController
    
        // Setup Bottom Nav
        val bottomNav: BottomNavigationView = binding.bottomNav
        bottomNav.setupWithNavController(navController)
    }

    I’d like to write a full tutorial on how to do this but I’m not sure if I could spare some time. If you’ve been reading my recent posts you’ll notice that I’m not able to put it so much effort in explaining everything. These days I’m always in a hurry to finish stuff. But I’ll still try!!

  • val vs. const //Kotlin Android Dev Diary

    Let’s start off by introducing val.

    To define a variable in Kotlin, you may use var or val:

    varval
    has a mutable value (changeable)has an immutable value (read-only)

    So we use val for immutable values, so what is the const keyword and what’s its difference from val?

    constval
    has an immutable value (read-only)has an immutable value (read-only)
    initialized at compile time onlyvalue may be assigned during runtime

    Other rules for const in Kotlin:

    • must be a top-level property or placed within a companion object
    • value must be a primitive type (can never have function or any class constructor as its value)
    • no custom getter
  • Error inflating class androidx.fragment.app.FragmentContainerView // Android Kotlin Dev Diary

    The cause of error in Logcat:

         Caused by: android.view.InflateException: Binary XML file line #9 in net.catzie.sampleapp:layout/activity_main: Binary XML file line #9 in net.catzie.sampleapp:layout/activity_main: Error inflating class androidx.fragment.app.FragmentContainerView
         Caused by: android.view.InflateException: Binary XML file line #9 in net.catzie.sampleapp:layout/activity_main: Error inflating class androidx.fragment.app.FragmentContainerView
         Caused by: java.lang.IllegalStateException: no start destination defined via app:startDestination for net.catzie.sampleapp:id/nav_graph

    The fix was to add a startDestination to <navigation> tag in the nav_graph.xml.