for Android
Documentation

Framework

An Android screen management library that eliminates Fragment boilerplate, ViewModel wiring, and back-stack plumbing — so you can focus entirely on business logic.

🧭

No more Fragment transactions

Navigation is a one-liner. Type-safe, with no string tags or resource IDs.

♻️

No more ViewModel factories

Every screen gets a scoped, rotation-safe coordinator automatically.

💾

No more onSaveInstanceState

State lives in SavedStateHandle and binds to views declaratively.

📬

No more callback wiring

Result propagation between any two screens — including dialogs — is built in.

🪟

Dialogs are just Screens

Present dialogs and bottom sheets through the same navigator interface.

🔙

Per-screen back handling

Override back press in any screen without touching the Activity.

How it works

Framework wraps Android's Fragment system with three core primitives:

  • FrameworkActivity — your single host. Owns the screen registry and implements Navigator.
  • Screen — your unit of UI. Owns its own state, lifecycle, and navigation context.
  • Navigator — the interface every Screen uses to move around the app.

Under the hood, each Screen is hosted inside a ScreenHostFragment managed by a ScreenCoordinator ViewModel. You never interact with either directly.

Built on standard Android. Framework does not replace the Fragment back stack or introduce a custom navigation graph. It wraps them cleanly — you can always drop down to raw Fragment APIs if needed.
Getting Started

Setup & Installation

Framework is distributed via JitPack. Add the repository and the dependency — that's it.

1. Add JitPack

Add JitPack to your dependency resolution in settings.gradle or settings.gradle.kts:

Groovy
Kotlin DSL
// settings.gradle
dependencyResolutionManagement {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}groovy
// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        maven("https://jitpack.io")
    }
}kotlin

2. Add the dependency

Groovy
Kotlin DSL
// app/build.gradle
dependencies {
    implementation 'com.github.AyazAlvi:framework:<version>'
}groovy
// app/build.gradle.kts
dependencies {
    implementation("com.github.AyazAlvi:framework:<version>")
}kotlin
Latest version: Check the JitPack badge at the top of the GitHub repository for the current release tag.

Requirements

  • Android API 21+
  • Kotlin 1.8+
  • ViewBinding enabled in your module
  • AndroidX AppCompat and Fragment KTX

Enable ViewBinding

Groovy
Kotlin DSL
android {
    buildFeatures {
        viewBinding true
    }
}groovy
android {
    buildFeatures {
        viewBinding = true
    }
}kotlin
Core Concepts

FrameworkActivity

The single host Activity for your entire app. It owns the screen registry, implements Navigator, and manages the Fragment back stack.

Basic setup

Extend FrameworkActivity and implement two things: your container ID, and screen registration.

class MainActivity : FrameworkActivity() {

    override val fragmentContainerId = R.id.container

    override fun onRegisterScreens(registry: ScreenRegistry) {
        registry.register(FragmentHomeBinding::inflate, ::HomeScreen)
        registry.register(FragmentDetailBinding::inflate, ::DetailScreen)
        registry.register(DialogConfirmBinding::inflate, ::ConfirmDialog)
        registry.register(SheetOptionsBinding::inflate, ::OptionsSheet)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if (savedInstanceState == null) {
            push<HomeScreen>()
        }
    }
}kotlin

Layout

Your layout just needs a container for screens. A FrameLayout works perfectly:

<FrameLayout
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />xml

Multiple containers

If your layout has multiple containers (e.g. a side panel on tablets), you can push screens into any of them by passing containerId:

navigator.push<DetailScreen>(containerId = R.id.panel_container)kotlin

Transitions

Framework automatically reads activityOpenEnterAnimation, activityOpenExitAnimation, and their close variants from your app theme. Define them in your theme to get free transition animations on every navigation:

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">
    <item name="android:activityOpenEnterAnimation">@anim/slide_in_right</item>
    <item name="android:activityOpenExitAnimation">@anim/slide_out_left</item>
    <item name="android:activityCloseEnterAnimation">@anim/slide_in_left</item>
    <item name="android:activityCloseExitAnimation">@anim/slide_out_right</item>
</style>xml
Core Concepts

Screen

The fundamental unit of UI in Framework. A Screen owns its ViewBinding, state, and navigation context. It is not a Fragment — it simply lives inside one.

Creating a Screen

class HomeScreen(context: ScreenContext) : Screen<FragmentHomeBinding>(context) {

    override fun onFirstLaunch() {
        // Called once — survives rotation. Put initialization logic here.
    }

    override fun onUI() {
        // Called every time the view is (re)created.
        // Wire clicks, bind state, set up adapters here.
        ui.btnNext.setOnClickListener {
            navigator.push<DetailScreen>()
        }
    }
}kotlin

onFirstLaunch vs onUI

MethodWhen it runsUse it for
onFirstLaunch()Once per Screen lifetimeFetching data, setting initial state, one-time setup
onUI()Every view creationBinding state, setting click listeners, configuring views
Rotation safe. onFirstLaunch() will not re-run on rotation. onUI() will, but state bound through state() is automatically re-applied to the new views.

Accessing the UI

Use the ui property anywhere inside your Screen to access your ViewBinding. It is only valid while the view is attached — accessing it outside of that window throws an IllegalStateException.

ui.tvTitle.text = "Hello"
ui.btnSave.setOnClickListener { save() }kotlin

Accessing context

// Access the host Activity (typed as FrameworkActivity)
val act = activity

// Access the host Fragment (for lifecycle owners, etc.)
val frag = fragmentkotlin
Core Concepts

Screen Registry

The registry is how Framework knows which ViewBinding inflater and which constructor to use for each Screen class. Every screen that will ever be shown must be registered.

Registering screens

override fun onRegisterScreens(registry: ScreenRegistry) {
    // Short form — works when factory is a simple constructor ref
    registry.register(FragmentHomeBinding::inflate, ::HomeScreen)

    // Long form — useful if you need to inject extra dependencies
    registry.register(
        inflateBlock = FragmentDetailBinding::inflate,
        factory = { context -> DetailScreen(context, myDependency) }
    )
}kotlin

What happens at registration

The registry stores a ScreenDefinition per class — a pair of the inflate function and the factory. When navigation triggers a screen, Framework looks up the definition, creates the binding, and calls the factory with a ScreenContext.

All screens must be registered. Pushing or presenting an unregistered screen throws an IllegalStateException at runtime.

ScreenDefinition

FieldTypeDescription
inflate(LayoutInflater, ViewGroup?) → ViewBindingThe binding inflater from your generated binding class
factory(ScreenContext) → Screen<*>Constructs your Screen with the provided context
Navigation

Dialogs & Bottom Sheets

Dialogs and bottom sheets are first-class screens. Register them exactly like any other screen and present them through the navigator.

Registering

registry.register(DialogConfirmBinding::inflate, ::ConfirmDialog)
registry.register(SheetOptionsBinding::inflate, ::OptionsSheet)kotlin

Presenting

navigator.presentDialog<ConfirmDialog>()
navigator.presentDialog<ConfirmDialog>(args = bundleOf("message" to "Are you sure?"))

navigator.presentBottomSheet<OptionsSheet>()
navigator.presentBottomSheet<OptionsSheet>(args = bundleOf("title" to "Pick one"))kotlin

Implementing a dialog Screen

class ConfirmDialog(context: ScreenContext) : Screen<DialogConfirmBinding>(context) {

    override fun onUI() {
        val message = arguments?.getString("message") ?: "Are you sure?"
        ui.tvMessage.text = message

        ui.btnCancel.setOnClickListener { close() }
        ui.btnConfirm.setOnClickListener {
            popWithResult("confirm", bundleOf("confirmed" to true))
        }
    }
}kotlin

Dismissing

Call close() from inside the screen — it automatically detects whether it's hosted in a dialog, bottom sheet, or regular fragment and dismisses correctly. Or dismiss explicitly from outside:

navigator.dismissCurrentDialog()
navigator.dismissCurrentBottomSheet()kotlin
Always use close() inside dialog screens. Calling navigator.pop() from a dialog screen will not dismiss it — it will attempt to pop the fragment back stack instead. close() is host-aware.
Navigation

Result Propagation

Pass data back from any screen — fragment, dialog, or bottom sheet — to its caller with a consistent API built on FragmentResultListener.

Push and listen

// From a parent Screen — push and set up the listener in one call
pushForResult<DetailScreen>(requestKey = "detail_result") { bundle ->
    val name = bundle.getString("name")
    ui.tvName.text = name
}kotlin

Send result and close

// From the child Screen
ui.btnSave.setOnClickListener {
    popWithResult(
        requestKey = "detail_result",
        result = bundleOf("name" to ui.etName.text.toString())
    )
}kotlin

With dialogs and bottom sheets

// Present a dialog and listen for its result
presentDialogForResult<ConfirmDialog>(requestKey = "confirm") { bundle ->
    if (bundle.getBoolean("confirmed")) deleteItem()
}

// Present a bottom sheet and listen for its result
presentBottomSheetForResult<OptionsSheet>(requestKey = "option_picked") { bundle ->
    val option = bundle.getString("option")
}kotlin

Manual API

If you need more control:

// Set a result without closing
setResult("my_key", bundleOf("data" to "value"))

// Listen for a result manually
listenForResult("my_key") { bundle -> /* handle */ }kotlin

API summary

MethodWhereDescription
pushForResult<S>(key, args?, onResult)Parent screenPush + register listener atomically
presentDialogForResult<S>(key, args?, onResult)Parent screenPresent dialog + register listener
presentBottomSheetForResult<S>(key, args?, onResult)Parent screenPresent sheet + register listener
popWithResult(key, bundle)Child screenSet result then close
setResult(key, bundle)Any screenSet result without closing
listenForResult(key, onResult)Any screenRegister a listener manually
Navigation

Back Press Handling

Override the back button for any individual screen without subclassing the Activity or managing OnBackPressedCallback yourself.

Enabling the override

class EditorScreen(context: ScreenContext) : Screen<FragmentEditorBinding>(context) {

    override fun onFirstLaunch() {
        backPressOverrideEnabled = true
    }

    override fun onBackPressed() {
        if (hasUnsavedChanges()) {
            presentDialog<DiscardDialog>()
        } else {
            navigator.performDefaultBack()
        }
    }
}kotlin

Enabling and disabling dynamically

You can toggle the override at any point — for example, only block back when a form is dirty:

ui.etContent.addTextChangedListener {
    backPressOverrideEnabled = it.toString().isNotEmpty()
}kotlin

performDefaultBack

Always call navigator.performDefaultBack() rather than navigator.pop() inside your override. It correctly handles the Activity finish case when the back stack is empty.

Dialogs respect back overrides too. Setting backPressOverrideEnabled = true on a dialog or bottom sheet screen also sets isCancelable = false, preventing dismissal on outside tap.
State & Data

State Management

State is stored in SavedStateHandle and survives both rotation and process death. Bind state keys to views — updates automatically dispatch to the main thread.

Declaring state

class ProfileScreen(context: ScreenContext) : Screen<FragmentProfileBinding>(context) {

    private val username by lazy { state("username", "") }
    private val isLoading by lazy { state("loading", false) }
    private val count by lazy { state("count", 0) }
}kotlin

Binding to views

override fun onUI() {

    username.bind(ui.tvName) { value, view ->
        view.text = value
    }

    isLoading.bind(ui.progressBar) { value, view ->
        view.visibility = if (value) View.VISIBLE else View.GONE
    }
}kotlin

Updating state

// Direct assignment — triggers all bound views immediately
username.value = "Ayaz"
isLoading.value = true
count.value++

// Safe from background threads too
viewModelScope.launch(Dispatchers.IO) {
    val data = repo.fetch()
    username.value = data.name  // dispatches to main thread automatically
}kotlin

Reading state

val currentName = username.value
val loading = isLoading.valuekotlin
Use by lazy. State is initialized lazily — declaring it at the top of your Screen with by lazy ensures the initial value is only written once, even after rotation. Without lazy, the initial value would overwrite the restored value on rotation.

How it works

Each ScreenState delegates to a key in the Screen's stateDataRegistry, which is kept in sync with SavedStateHandle on every write. When onUI() runs after rotation, all existing state is immediately re-applied to newly bound views.

State & Data

Arguments

Pass data into a Screen at push time via a Bundle. Access it through the arguments property.

Passing arguments

navigator.push<DetailScreen>(
    args = bundleOf(
        "userId" to "abc123",
        "position" to 5,
        "isEditable" to true
    )
)kotlin

Receiving arguments

class DetailScreen(context: ScreenContext) : Screen<FragmentDetailBinding>(context) {

    override fun onFirstLaunch() {
        val userId = arguments?.getString("userId")
        val position = arguments?.getInt("position", 0)
        val isEditable = arguments?.getBoolean("isEditable", false)

        // Load data using these args
        loadUser(userId)
    }
}kotlin
Read arguments in onFirstLaunch(), not onUI(). Arguments are immutable input — read them once and store the result in state if you need to display it.
State & Data

Shared ViewModels

Share a single ViewModel instance across multiple screens in the same Activity using sharedViewModel().

Defining a shared ViewModel

class AppViewModel : ViewModel() {
    val cart = MutableLiveData<List<Item>>(emptyList())

    fun addItem(item: Item) {
        cart.value = (cart.value ?: emptyList()) + item
    }
}kotlin

Accessing from multiple screens

class ProductScreen(context: ScreenContext) : Screen<FragmentProductBinding>(context) {
    private val vm: AppViewModel by lazy { sharedViewModel() }

    override fun onUI() {
        ui.btnAdd.setOnClickListener { vm.addItem(currentItem) }
    }
}

class CartScreen(context: ScreenContext) : Screen<FragmentCartBinding>(context) {
    private val vm: AppViewModel by lazy { sharedViewModel() }

    override fun onUI() {
        vm.cart.observe(fragment.viewLifecycleOwner) { items ->
            adapter.submitList(items)
        }
    }
}kotlin
Scoped to the Activity. sharedViewModel() uses ViewModelProvider(activity) — the same instance is returned for the same ViewModel class across all screens in the same Activity. It is cleared when the Activity is destroyed.
Utilities

bundleOf

A type-safe Bundle builder included with Framework. Covers all standard types with a clean vararg pair syntax.

Usage

val bundle = bundleOf(
    "id" to 42,
    "name" to "Ayaz",
    "active" to true,
    "score" to 9.5f,
    "tags" to arrayOf("android", "kotlin"),
    "nullableField" to null  // removes the key
)kotlin

Supported types

TypeNotes
String, CharSequence
Int, Long, Float, Double, Short, Byte, Char, BooleanAll primitives
IntArray, LongArray, FloatArray, BooleanArray, ByteArray, ShortArray, CharArrayPrimitive arrays
Array<String>, Array<Parcelable>Object arrays
ParcelableAny Parcelable object
SerializableFallback for Serializable objects
nullRemoves the key from the Bundle
Passing an unsupported type throws IllegalArgumentException at runtime. For custom objects, implement Parcelable (preferred) or Serializable.
Reference

Lifecycle Reference

Understanding when each Screen method is called helps you put logic in the right place.

Screen lifecycle

EventScreen methodFragment equivalent
First creation (pre-view)onFirstLaunch()onCreate() — first time only
View createdonUI()onViewCreated()
View destroyedonDestroyView()
Screen fully destroyedonCleared()onDestroy() + ViewModel.onCleared()

State restoration sequence

  1. Host Fragment is recreated after rotation
  2. ScreenCoordinator ViewModel is retrieved — same instance, so Screen is retained
  3. onFirstLaunch() does not run again
  4. New view is inflated, onUI() runs
  5. All state keys in stateDataRegistry are re-applied to the new view bindings immediately

UI availability

The ui property is only valid between onUI() and the next onDestroyView(). Accessing it outside this window throws IllegalStateException. When updating state from background threads, the binding is checked for null before dispatching — safe to call at any time.

Don't store view references as Screen properties. Let state bindings handle view updates — they are automatically cleared and re-established on each view recreation.
Reference

Full API Reference

Screen<VB>

MemberTypeDescription
uiVBThe current ViewBinding. Only valid while view is attached.
navigatorNavigatorNavigation interface for this screen.
argumentsBundle?Arguments passed at push time.
savedStateHandleSavedStateHandleDirect access to the state handle.
activityFrameworkActivityThe host Activity.
fragmentFragmentThe host Fragment.
backPressOverrideEnabledBooleanEnable/disable custom back handling.
onFirstLaunch()open funCalled once on first creation.
onUI()abstract funCalled every time the view is created.
onBackPressed()open funOverride for custom back behavior.
state(key, initialValue)ScreenState<T>Declare a piece of state.
close()funSmart close — detects dialog/sheet/fragment.
setResult(key, bundle)funEmit a result without closing.
popWithResult(key, bundle)funEmit a result and close.
listenForResult(key, onResult)funSubscribe to a result.
sharedViewModel<VM>()funGet or create an Activity-scoped ViewModel.

ScreenState<T>

MemberDescription
value: TGet or set the current value. Setting triggers all bound views.
bind(view, bindingBlock)Bind this state to a view. Re-applied automatically on view recreation.

Navigator (interface)

MethodDescription
push<S>(args?, sharedElement?, containerId?)Push screen onto back stack.
replace<S>(args?, containerId?)Replace current screen, no back stack entry.
pop()Pop back stack or finish Activity.
popToRoot()Pop all entries back to first screen.
performDefaultBack()Execute default back behavior.
presentDialog<S>(args?)Show a DialogFragment-hosted screen.
presentBottomSheet<S>(args?)Show a BottomSheetDialogFragment-hosted screen.
dismissCurrentDialog()Dismiss the topmost dialog.
dismissCurrentBottomSheet()Dismiss the topmost bottom sheet.

ScreenRegistry

MethodDescription
register<S>(inflateBlock, factory)Register a Screen with its binding inflater and constructor.

Top-level extensions on Screen

FunctionDescription
pushForResult<S>(key, args?, onResult)Push + register result listener atomically.
presentDialogForResult<S>(key, args?, onResult)Present dialog + register result listener.
presentBottomSheetForResult<S>(key, args?, onResult)Present sheet + register result listener.
state(key, initialValue)Declare a ScreenState on this Screen.
sharedViewModel<VM>()Get an Activity-scoped ViewModel instance.

bundleOf

SignatureDescription
bundleOf(vararg pairs: Pair<String, Any?>): BundleType-safe Bundle builder. Null values remove the key.