Lich Android Libraries

repository·master·Indexed 41 days ago

https://github.com/line/lich

A collection of lightweight Android libraries providing a Service Locator framework for managing singleton components. Lich simplifies component management, lifecycle handling, and networking (OkHttp/Thrift) without requiring configuration files, annotations, or DSLs. It features lazy acquisition via Kotlin delegated properties, support for Jetpack Compose, and multi-module dependency resolution using delegateCreation() and delegateToServiceLoader().

Tokens
16.7K
Snippets
45
Records
52
Agent score
51%

What's inside Lich

  1. Overview of Lich libraries

    master
    Lich is a collection of Android development libraries designed to enhance app development. The libraries focus on lightweight frameworks for component management, lifecycle handling, and networking extensions. All libraries are hosted on Maven Central under the group ID com.linecorp.lich.
  2. Understand the Lich sample application architecture

    master

    The Lich sample application is a multi-module project designed to demonstrate the use of Lich libraries within an MVVM (Model-View-ViewModel) architecture. It is organized into the following functional modules:

    • base: Contains base classes and shared resources used across the entire application.
    • model-api: Defines the interface/API for the Model layer.
    • model-impl: Provides the concrete implementation of the model-api.
    • ui: Implements the ViewModel and View layers of the MVVM architecture.
    • app: The entry point module that orchestrates and binds all other modules together.
  3. Explore the sample Thrift protocol implementation

    master

    The sample_thrift directory contains a reference implementation of a Thrift protocol. This can be used to understand how to define services and data structures in Thrift and how the resulting Java code is structured within the Lich ecosystem.

    • Thrift Definition: The service contract is defined in src/main/thrift/FooService.thrift.
    • Generated Java Code: The code generated from the Thrift definition can be found in src/main/java/com/linecorp/lich/sample/thrift/.
  4. Use AutoResetLifecycleScope for safer Flow collection

    master

    AutoResetLifecycleScope is a safer alternative to AndroidX lifecycleScope. While lifecycleScope coroutines are only cancelled when the Lifecycle is DESTROYED, AutoResetLifecycleScope can be configured to automatically cancel coroutines when the Lifecycle is STOPPED (using ResetPolicy.ON_STOP).

    This prevents resource leaks common when using lifecycleScope.launchWhenStarted to collect Flows, as those coroutines remain active even when the Lifecycle is stopped.

    Key behaviors:

    • Bound to Dispatchers.Main.
    • Cancelled when the Lifecycle is DESTROYED.
    • Automatically cancelled when the Lifecycle is STOPPED if ResetPolicy.ON_STOP is used.
    • Note: When using in a Fragment, do not use viewLifecycleOwner; use the Fragment instance itself.
    class FooFragment : Fragment() {
    
        // Any coroutines launched from this scope are automatically cancelled when FooFragment is STOPPED.
        // NOTE: Don't use `viewLifecycleOwner` here.
        private val autoResetLifecycleScope: CoroutineScope = AutoResetLifecycleScope(this)
    
        // A repository that provides some data as a Flow.
        private val fooRepository = FooRepository()
    
        override fun onStart() {
            super.onStart()
    
            // It is SAFE to collect a flow with AutoResetLifecycleScope.
            // The coroutine launched here will be automatically cancelled just before `FooFragment.onStop()`.
            autoResetLifecycleScope.launch {
                fooRepository.dataFlow().collect { value ->
                    textView.text = value
                }
            }
        }
    }
  5. Available Lich libraries

    master

    Lich provides several specialized libraries for Android development:

    • component: A lightweight framework for managing singleton components within Android applications.
    • viewmodel: A lightweight framework for managing ViewModels, following the same pattern as the component library.
    • savedstate: Provides type-safe access to Android's saved instance state.
    • lifecycle: A small utility library for working with Android Jetpack Lifecycle.
    • okhttp: Provides Coroutine-aware extensions for the OkHttp networking library.
    • thrift: Enables using Apache Thrift in combination with OkHttp.
  6. How Lich components work

    master

    Lich is a Service Locator framework for managing singleton components on Android. It is not a Dependency Injection (DI) framework; it uses no configuration files, annotations (except AutoService), or DSLs. Instead, dependencies are defined programmatically by implementing a ComponentFactory in a class's companion object.

    Key characteristics:

    • Fast: Runtime performance is comparable to Dagger2.
    • Low Footprint: ~3kB in classes after R8 optimization.
    • Zero Config: No global settings or initialization required.
    • Lazy Acquisition: Uses Kotlin delegated properties for easy lazy loading.
  7. Use Coroutines in AbstractViewModel

    master

    The AbstractViewModel class implements CoroutineScope and is bound to Dispatchers.Main. This scope is automatically cancelled just before onCleared() is called. You can launch coroutines directly using launch { ... } without needing to reference a specific scope variable, similar to AndroidX viewModelScope.

    class BarViewModel(private val barRepository: BarRepository) : AbstractViewModel() {
    
        val barText: MutableLiveData<String> = MutableLiveData("")
    
        fun loadBarText() {
            // The launched job will be automatically cancelled when this ViewModel is destroyed.
            launch {
                val barData = barRepository.loadBarData()
                barText.value = barData.text
            }
        }
    }
  8. Resolve circular dependencies using lazy acquisition

    master

    If two components depend on each other (e.g., ComponentX needs ComponentY and vice versa), direct instantiation in the constructor will cause a circular dependency error.

    To resolve this, use lazy acquisition by using the by context.component(Type) property delegate instead of calling context.getComponent(Type) inside the constructor. This defers the resolution of the dependency until it is actually accessed.

    class ComponentX(context: Context) {
        // Use 'by context.component' for lazy resolution
        private val componentY by context.component(ComponentY)
    
        companion object : ComponentFactory<ComponentX>() {
            override fun createComponent(context: Context): ComponentX = ComponentX(context)
        }
    }
    
    class ComponentY(context: Context) {
        private val componentX by context.component(ComponentX)
    
        companion object : ComponentFactory<ComponentY>() {
            override fun createComponent(context: Context): ComponentY = ComponentY(context)
        }
    }
  9. Implement multi-module dependency resolution with delegateCreation()

    master

    In multi-module projects where a 'base' module needs to access features in a 'foo' module without a direct dependency, you can use the Facade pattern combined with delegateCreation().

    1. Define a Facade interface in the 'base' module.
    2. In the 'base' module's ComponentFactory, use delegateCreation(context, "full.package.path.ToFactory") to point to the implementation factory.
    3. In the 'foo' module, implement a class that inherits from DelegatedComponentFactory with a public empty constructor. This class is responsible for instantiating the actual implementation.
    // "base" module
    interface FooModuleFacade {
        fun launchFooActivity()
    
        companion object : ComponentFactory<FooModuleFacade>() {
            override fun createComponent(context: Context): FooModuleFacade =
                delegateCreation(context, "module.foo.FooModuleFacadeFactory")
        }
    }
    
    // "foo" module
    class FooModuleFacadeFactory: DelegatedComponentFactory<FooModuleFacade>() {
        override fun createComponent(context: Context): FooModuleFacade = 
            FooModuleFacadeImpl(context)
    }
    
    internal class FooModuleFacadeImpl(private val context: Context) : FooModuleFacade {
        override fun launchFooActivity() { /* ... */ }
    }
  10. Install Lich OkHttp

    master

    Add the following dependency to your build.gradle file to use coroutine-aware extensions for OkHttp:

    // build.gradle
    dependencies {
        implementation 'com.linecorp.lich:okhttp:x.x.x'
    }
    implementation 'com.linecorp.lich:okhttp:x.x.x'
  11. Mock ViewModels in Unit Tests

    master

    Lich provides mockViewModel(ViewModelClass) to easily mock ViewModels in tests. This works with both MockK and Mockito-Kotlin.

    Note: In Robolectric tests, all ViewModel mocks are automatically cleared. For other test types, you should call clearAllMockViewModels() in your @After block to prevent leakages between tests.

    // MockK Example
    @Test
    fun testViewBinding() {
        val mockBarText = MutableLiveData("Mocked.")
        val mockViewModelHandle = mockViewModel(BarViewModel) {
            every { barText } returns mockBarText
        }
        // ... use mockViewModelHandle.mock to interact with the mock
    }
    
    // Mockito-Kotlin Example
    @Test
    fun testViewBinding() {
        val mockBarText = MutableLiveData("Mocked.")
        val mockViewModelHandle = mockViewModel(BarViewModel) {
            on { barText } doReturn mockBarText
        }
        // ...
    }
  12. Generate ViewModelArgs for type-safe initialization

    master

    Lich can automatically generate ViewModelArgs classes to handle the boilerplate of initializing a SavedStateHandle from a Bundle.

    1. Annotate your ViewModel

    Use @GenerateArgs on the ViewModel class and @Argument on the properties you want included in the generated arguments class.

    @GenerateArgs
    class FooViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    
        @Argument
        private val userName: String by savedStateHandle.required()
    
        @Argument(isOptional = true)
        private val tags: Array<String> by savedStateHandle.initial(arrayOf("normal"))
    
        @Argument
        private var attachment: Parcelable? by savedStateHandle
    
        @Argument
        private val message: MutableLiveData<CharSequence> by savedStateHandle.liveData()
    }

    2. Use the generated class

    The compiler generates a class (e.g., FooViewModelArgs) that implements ViewModelArgs. You can use this to pass arguments to a Fragment or Activity.

    @GenerateArgs
    class FooViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    
        @Argument
        private val userName: String by savedStateHandle.required()
    
        @Argument(isOptional = true)
        private val tags: Array<String> by savedStateHandle.initial(arrayOf("normal"))
    
        @Argument
        private var attachment: Parcelable? by savedStateHandle
    
        @Argument
        private val message: MutableLiveData<CharSequence> by savedStateHandle.liveData()
    }