Overview
The presentation layer (io.muun.apollo.presentation) contains all UI code for Muun Wallet. It implements the Model-View-Presenter (MVP) pattern and strictly depends only on the domain layer.
Critical Architecture Rule : The presentation layer never directly accesses the data layer. All data operations go through domain actions.
Layer Structure
presentation/
├── app/ # Application setup and infrastructure
│ ├── di/ # Dependency injection (Dagger)
│ ├── startup/ # App initialization
│ └── ApolloApplication.kt
├── ui/ # User interface components
│ ├── base/ # Base classes (BaseActivity, BasePresenter)
│ ├── activity/ # Activities and extensions
│ ├── fragments/ # Feature fragments
│ ├── adapter/ # RecyclerView adapters
│ ├── bundler/ # State bundlers
│ └── [features]/ # Feature-specific UI
├── model/ # Presentation models
├── biometrics/ # Biometric authentication
├── export/ # Export functionality (PDF, etc.)
└── sensors/ # Sensor integrations
MVP Pattern
Muun implements Model-View-Presenter with clear separation:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Model │◄────│ Presenter │─────►│ View │
│ │ │ │ │ │
│ Domain │ │ Business │ │ UI │
│ Models │ │ Logic │ │ Components │
└─────────────┘ └─────────────┘ └─────────────┘
▲ ▲ ▲
│ │ │
Domain Layer Presentation Activities/
Layer Fragments
Model
Domain models from the domain layer
Presentation models that adapt domain models for UI
No business logic
View
Activities and Fragments
Implements view interfaces
Displays data
Captures user input
No business logic
Presenter
Coordinates between view and model
Handles user interactions
Calls domain actions
Manages view state
No Android UI code
Base Classes
BaseActivity
All activities extend BaseActivity, which provides common functionality:
public abstract class BaseActivity < PresenterT extends Presenter >
extends ExtensibleActivity
implements BaseView {
private ViewBinding _binding ;
@ Inject
@ NotNull
protected PresenterT presenter ;
@ Inject
protected ApplicationLockExtension applicationLockExtension ;
@ Inject
ExternalResultExtension externalResultExtension ;
@ Inject
PermissionManagerExtension permissionManagerExtension ;
@ Override
protected void onCreate ( Bundle savedInstanceState ) {
super . onCreate (savedInstanceState);
// Dependency injection
injectPresenter ();
// State restoration
Icepick . restoreInstanceState ( this , savedInstanceState);
// Setup presenter
presenter . setUp ( getArgumentsBundle ());
}
@ Override
protected void onResume () {
super . onResume ();
presenter . onResume ();
}
@ Override
protected void onPause () {
super . onPause ();
presenter . onPause ();
}
@ Override
protected void onDestroy () {
presenter . tearDown ();
super . onDestroy ();
}
}
Example from: presentation/ui/base/BaseActivity.java:68
BaseActivity handles common concerns like dependency injection, lifecycle management, and state restoration, allowing feature activities to focus on their specific functionality.
BasePresenter
All presenters extend BasePresenter:
public abstract class BasePresenter < ViewT extends BaseView > {
protected ViewT view ;
protected CompositeSubscription compositeSubscription ;
public void setUp (@ NotNull Bundle arguments ) {
compositeSubscription = new CompositeSubscription ();
}
public void setView (@ NotNull ViewT view ) {
this . view = view;
}
public void onResume () {
// Override in subclasses
}
public void onPause () {
// Override in subclasses
}
public void tearDown () {
if (compositeSubscription != null ) {
compositeSubscription . unsubscribe ();
}
}
protected void subscribeTo ( Observable < ? > observable , Observer < ? > observer ) {
compositeSubscription . add (
observable . subscribe (observer)
);
}
}
The compositeSubscription automatically unsubscribes from all RxJava streams when the presenter is torn down, preventing memory leaks.
BaseView Interface
public interface BaseView {
void showError ( UserFacingError error );
void showLoading ();
void hideLoading ();
void finishActivity ();
}
Activity Extensions
Muun uses a novel extension pattern to add cross-cutting functionality to activities:
activity/extension/
├── ApplicationLockExtension # App lock/unlock
├── PermissionManagerExtension # Runtime permissions
├── ExternalResultExtension # Activity results
├── AlertDialogExtension # Dialogs
├── SnackBarExtension # Snackbars
├── ScreenshotBlockExtension # Screenshot prevention
├── NfcReaderModeExtension # NFC sessions
└── ShakeToDebugExtension # Debug tools
Extension Example: ApplicationLockExtension
public class ApplicationLockExtension {
private final ApplicationLockManager lockManager ;
@ Inject
public ApplicationLockExtension ( ApplicationLockManager lockManager ) {
this . lockManager = lockManager;
}
public void onResume ( Activity activity ) {
if ( lockManager . shouldLock ()) {
showLockScreen (activity);
}
}
private void showLockScreen ( Activity activity ) {
Intent intent = new Intent (activity, PinActivity . class );
activity . startActivity (intent);
}
}
Extensions are injected via Dagger and automatically available in all activities that extend BaseActivity.
Dependency Injection
Application Component
@Singleton
@Component (modules = [
ApplicationModule:: class ,
DataModule:: class ,
DomainModule:: class ,
BiometricsModule:: class ,
StartupModule:: class
])
interface ApplicationComponent {
fun inject (application: ApolloApplication )
// Expose for activity components
fun userRepository (): UserRepository
fun operationActions (): OperationActions
// ... more dependencies
}
Example from: presentation/app/di/ApplicationComponent.kt
Activity Component
Each activity can have its own component:
@ActivityScope
@Component (
dependencies = [ApplicationComponent:: class ],
modules = [HomeModule:: class ]
)
interface HomeComponent {
fun inject (activity: HomeActivity )
}
Injection in Activities
public class HomeActivity extends BaseActivity < HomePresenter > {
@ Override
protected void injectPresenter () {
DaggerHomeComponent . builder ()
. applicationComponent ( getApplicationComponent ())
. build ()
. inject ( this );
}
}
Feature Structure Example: Home Screen
HomeActivity
public class HomeActivity extends BaseActivity < HomePresenter >
implements HomeView {
private HomeBinding binding ;
@ Override
protected void onCreate ( Bundle savedInstanceState ) {
super . onCreate (savedInstanceState);
binding = HomeBinding . inflate ( getLayoutInflater ());
setContentView ( binding . getRoot ());
setupViews ();
}
private void setupViews () {
binding . sendButton . setOnClickListener (v ->
presenter . onSendClicked ()
);
binding . receiveButton . setOnClickListener (v ->
presenter . onReceiveClicked ()
);
}
@ Override
public void showBalance ( MonetaryAmount balance ) {
binding . balanceText . setText (
balance . toFriendlyString ()
);
}
@ Override
public void showOperations ( List < UiOperation > operations ) {
adapter . setOperations (operations);
}
@ Override
public void navigateToSend () {
Navigator . navigateToSend ( this );
}
}
HomePresenter
public class HomePresenter extends BasePresenter < HomeView > {
private final OperationActions operationActions ;
private final BalanceSelector balanceSelector ;
private final UserRepository userRepository ;
@ Inject
public HomePresenter ( OperationActions operationActions ,
BalanceSelector balanceSelector ,
UserRepository userRepository ) {
this . operationActions = operationActions;
this . balanceSelector = balanceSelector;
this . userRepository = userRepository;
}
@ Override
public void setUp (@ NotNull Bundle arguments ) {
super . setUp (arguments);
// Watch balance changes
subscribeTo (
balanceSelector . watchBalance (),
balance -> view . showBalance (balance)
);
// Watch operations
subscribeTo (
operationActions . watchOperations (),
operations -> view . showOperations (
toUiOperations (operations)
)
);
}
public void onSendClicked () {
userRepository . getUser ()
. subscribe (user -> {
if ( user . hasPassword ) {
view . navigateToSend ();
} else {
view . showError ( new AuthenticationRequiredError ());
}
});
}
public void onReceiveClicked () {
view . navigateToReceive ();
}
private List < UiOperation > toUiOperations ( List < Operation > operations ) {
return operations . stream ()
. map (UiOperation :: fromOperation)
. collect ( Collectors . toList ());
}
}
HomeView Interface
public interface HomeView extends BaseView {
void showBalance ( MonetaryAmount balance );
void showOperations ( List < UiOperation > operations );
void navigateToSend ();
void navigateToReceive ();
}
Presentation Logic Flow :
User clicks button → Activity calls presenter method
Presenter executes domain action
Domain action returns Observable with result
Presenter subscribes and updates view
View displays updated data
Presentation Models
Presentation models adapt domain models for UI display:
UiOperation
data class UiOperation (
val id: Long ,
val description: String ,
val amount: String ,
val formattedAmount: CharSequence ,
val date: String ,
val status: OperationStatus ,
val direction: Direction ,
val iconRes: Int ,
val statusColor: Int
) {
companion object {
fun fromOperation (operation: Operation ): UiOperation {
return UiOperation (
id = operation.id,
description = formatDescription (operation),
amount = operation.amount. toFriendlyString (),
formattedAmount = formatWithColor (operation),
date = formatDate (operation.createdAt),
status = operation.status,
direction = operation.direction,
iconRes = getIcon (operation),
statusColor = getStatusColor (operation)
)
}
}
}
Example from: presentation/model/UiOperation.kt
Presentation models handle formatting, color selection, and icon selection, keeping this display logic out of domain models and activities.
Navigation
The Navigator class centralizes navigation logic:
public class Navigator {
public static void navigateToSend ( Activity context ) {
Intent intent = new Intent (context, SendActivity . class );
context . startActivity (intent);
}
public static void navigateToReceive ( Activity context ) {
Intent intent = new Intent (context, ReceiveActivity . class );
context . startActivity (intent);
}
public static void navigateToOperationDetail (
Activity context ,
long operationId
) {
Intent intent = new Intent (context, OperationDetailActivity . class );
intent . putExtra (EXTRA_OPERATION_ID, operationId);
context . startActivity (intent);
}
public static void navigateToHome (
Activity context ,
boolean clearStack
) {
Intent intent = new Intent (context, HomeActivity . class );
if (clearStack) {
intent . setFlags (
Intent . FLAG_ACTIVITY_NEW_TASK |
Intent . FLAG_ACTIVITY_CLEAR_TASK
);
}
context . startActivity (intent);
}
}
Example from: presentation/app/Navigator.java
View Components
Custom Views
Muun uses custom views for reusable UI components:
ui/view/
├── MuunButton # Styled button
├── MuunTextInput # Text input with validation
├── MuunAmountInput # Bitcoin amount input
├── MuunHeader # Standard header
└── OperationListItem # Operation list item
RecyclerView Adapters
public class OperationsAdapter
extends RecyclerView.Adapter < OperationViewHolder > {
private List < UiOperation > operations = new ArrayList <>();
private OnOperationClickListener listener ;
public void setOperations ( List < UiOperation > operations ) {
this . operations = operations;
notifyDataSetChanged ();
}
@ Override
public OperationViewHolder onCreateViewHolder (
ViewGroup parent ,
int viewType
) {
View view = LayoutInflater . from ( parent . getContext ())
. inflate ( R . layout . item_operation , parent, false );
return new OperationViewHolder (view);
}
@ Override
public void onBindViewHolder ( OperationViewHolder holder , int position ) {
UiOperation operation = operations . get (position);
holder . bind (operation, listener);
}
public interface OnOperationClickListener {
void onOperationClicked ( UiOperation operation );
}
}
State Management
Icepick for State Preservation
Muun uses Icepick to save/restore activity state:
public class SendActivity extends BaseActivity < SendPresenter > {
@ State
String address ;
@ State
MonetaryAmount amount ;
@ Override
protected void onCreate ( Bundle savedInstanceState ) {
super . onCreate (savedInstanceState);
// Icepick automatically restores @State fields
Icepick . restoreInstanceState ( this , savedInstanceState);
}
@ Override
protected void onSaveInstanceState ( Bundle outState ) {
super . onSaveInstanceState (outState);
// Icepick automatically saves @State fields
Icepick . saveInstanceState ( this , outState);
}
}
RxJava Subscription Management
Presenters manage subscriptions to prevent leaks:
public void subscribeToBalance () {
subscribeTo (
balanceSelector . watchBalance (),
new Subscriber < MonetaryAmount >() {
@ Override
public void onNext ( MonetaryAmount balance ) {
view . showBalance (balance);
}
@ Override
public void onError ( Throwable error ) {
view . showError ( handleError (error));
}
@ Override
public void onCompleted () {
// Stream completed
}
}
);
}
Always use subscribeTo() instead of direct .subscribe() to ensure subscriptions are properly cleaned up when the presenter is destroyed.
Error Handling
Displaying Errors to Users
public void handleError ( Throwable error) {
if (error instanceof UserFacingError) {
view . showError ((UserFacingError) error);
} else if (error instanceof NetworkError) {
view . showError ( new NetworkConnectionError ());
} else {
view . showError ( new UnexpectedError ());
Timber . e (error, "Unexpected error" );
}
}
Error Dialog
public void showError ( UserFacingError error) {
new MuunDialog. Builder ()
. title ( R . string . error_title )
. message ( error . getUserFacingMessage ())
. positiveButton ( R . string . ok )
. show ( this );
}
Biometric Authentication
The biometrics package handles fingerprint/face authentication:
class BiometricsControllerImpl @Inject constructor (
private val applicationLockManager: ApplicationLockManager
) : BiometricsController {
override fun authenticate (
activity: FragmentActivity ,
callback: BiometricCallback
) {
val promptInfo = BiometricPrompt.PromptInfo. Builder ()
. setTitle ( "Unlock Muun" )
. setNegativeButtonText ( "Use PIN" )
. build ()
val biometricPrompt = BiometricPrompt (
activity,
ContextCompat. getMainExecutor (activity),
authenticationCallback (callback)
)
biometricPrompt. authenticate (promptInfo)
}
}
Example from: presentation/biometrics/BiometricsControllerImpl.kt
Export Functionality
The export package handles data export (Emergency Kit PDF):
class PdfExporter @Inject constructor (
private val fileManager: FileManager ,
private val context: Context
) {
fun exportEmergencyKit (
emergencyKit: EmergencyKit ,
outputFile: File
): Single < File > {
return Single. fromCallable {
val document = Document ()
PdfWriter. getInstance (document, FileOutputStream (outputFile))
document. open ()
// Add emergency kit content
addHeader (document)
addRecoveryCode (document, emergencyKit.code)
addInstructions (document)
document. close ()
outputFile
}
}
}
Example from: presentation/export/PdfExporter.kt
Testing Presentation Layer
Presenter Tests
public class HomePresenterTest {
@ Mock
private HomeView view ;
@ Mock
private OperationActions operationActions ;
@ Mock
private BalanceSelector balanceSelector ;
private HomePresenter presenter ;
@ Before
public void setUp () {
MockitoAnnotations . initMocks ( this );
presenter = new HomePresenter (
operationActions,
balanceSelector,
userRepository
);
presenter . setView (view);
}
@ Test
public void setUp_loadsBalanceAndOperations () {
// Given
MonetaryAmount balance = btc ( 1.5 );
when ( balanceSelector . watchBalance ())
. thenReturn ( Observable . just (balance));
// When
presenter . setUp ( new Bundle ());
// Then
verify (view). showBalance (balance);
}
@ Test
public void onSendClicked_withAuthentication_navigates () {
// Given
User user = createUserWithPassword ();
when ( userRepository . getUser ())
. thenReturn ( Observable . just (user));
// When
presenter . onSendClicked ();
// Then
verify (view). navigateToSend ();
}
}
Best Practices
Presentation Layer Guidelines
No Business Logic : Business decisions happen in domain layer
View Interfaces : Activities implement view interfaces
Presenter Testing : Test presenters with mocked views
Subscription Management : Use subscribeTo() for RxJava
State Preservation : Use Icepick for activity state
Navigation Centralization : Use Navigator class
Error Handling : Convert all errors to user-facing messages
Common Pitfalls
Anti-Patterns to Avoid
❌ Business logic in Activities or Presenters
❌ Direct data layer access from presentation
❌ Unmanaged RxJava subscriptions
❌ Not implementing view interfaces
❌ Tight coupling between Activity and Presenter
❌ Hardcoded navigation
Domain Layer Learn how presenters use domain actions
MVP Pattern Understand MVP in clean architecture