Android

There is no special support required to use Ballast in native Android applications. It works with both Compose and traditional XML View-based screens, as well as Activity-, Fragment-, or pure-Compose-based screens/navigation.

Usage

AndroidViewModel

Ballast offers AndroidViewModel, which is a subclass of androidx.lifecycle.ViewModel and uses the viewModelScope to control the ViewModel's lifecycle. Subclasses of AndroidViewModel can be scoped to Activities, Fragments, or NavGraphs as usual, and also work with Hilt's @AndroidViewModel injection. There is also a AndroidBallastRepository which extends androidx.lifecycle.ViewModel as the Android-specific analog of BallastRepository from the Ballast Repository module.

An AndroidViewModel intentionally does not have access to the Activity or Fragment it is typically associated with when created or during Hilt injection, as it lives longer than the associated Activity/Fragment. Thus, it is not possible to provide the EventHandler directly an instance of AndroidViewModel with Hilt. It will have to be attached dynamically with vm.attachEventHandler() after creation. In a View-based screen, this would be attached in a Fragment's onViewCreated() callback or an Activity's onStart() or onResume() callbacks. In either case, the EventHandler itself will only be active during the RESUMED state, and collected safely with repeatOnLifecycle. Within Compose, you can call vm.attachEventHandler() within a LaunchedEffect to handle events on the coroutineScope of a particular Composable function.

Other

if you need to control its lifecycle with another CoroutineScope (such as when scoping the ViewModel to a Compose function), you can use the normal BasicViewModel as your ViewModel implementation. The BasicViewModel is unrelated to androidx.lifecycle.ViewModel, and thus it cannot be provided from any of the normal Android ViewModel mechanisms, but gives you more flexibility over the lifetime of the ViewModel.

Examples

XML Views

@AndroidEntryPoint
class ExampleFragment : ComposeFragment() {

    @Inject
    lateinit var eventHandler: ExampleEventHandler.Factory

    private val viewModel: ExampleViewModel by viewModels()
    
    private var binding: FragmentExampleBinding? = null

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return FragmentExampleBinding
            .inflate(inflater, container, false)
            .also { binding = it }
            .root
    }

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

        // events are sent back to the screen during the Fragment's Lifecycle RESUMED state
        viewModel.attachEventHandlerOnLifecycle(
            this,
            eventHandler.create(this, findNavController()),
        )
        
        // Collect the state on the Fragment's Lifecycle RESUMED state, updating the entire UI with each change
        vm.observeStatesOnLifecycle(this) { state -> 
            binding?.updateWithState(state) { viewModel.trySend(it) } 
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        binding = null
    }

    private fun FragmentExampleBinding.updateWithState(
        state: ExampleContract.State,
        postInput: (ExampleContract.Inputs) -> Unit
    ) {
        tvCounter.text = "${state.count}"

        btnDec.setOnClickListener { postInput(ExampleContract.Inputs.Decrement(1)) }
        btnInc.setOnClickListener { postInput(ExampleContract.Inputs.Increment(1)) }
    }
}

Compose

If you're writing a pure Compose Android application, see the Compose page for integration with using BasicViewModel. But if you're developing a hybrid app which uses Activities or Fragments for navigation and Compose views within them, you'll probably want to use AndroidViewModel and inject the ViewModels with Hilt, and the integration process will need to handle some additional features like dynamically attaching/removing the EventHandler.

@AndroidEntryPoint
class ExampleFragment : ComposeFragment() {

    @Inject
    lateinit var eventHandler: ExampleEventHandler.Factory

    private val viewModel: ExampleViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return ComposeView(requireContext()).apply {
            setContent {
                MaterialTheme {
                    val uiState by viewModel.observeStates().collectAsState()
                    
                    LaunchedEffect(viewModel, eventHandler) {
                        viewModel.attachEventHandler(
                            this,
                            eventHandler.create(this, findNavController())
                        )
                        viewModel.trySend(ExampleContract.Inputs.Initialize)
                    }
                    
                    ExampleContent(uiState) {
                        viewModel.trySend(it)
                    }
                }
            }
        }
    }

    @Composable
    fun ExampleContent(
        uiState: ExampleContract.State,
        postInput: (ExampleContract.Inputs) -> Unit,
    ) {
        // ...
    }
}