# Native Android — Java Reference ## When to Use Java Java remains fully supported by Android and Google. Use it when: - Maintaining or extending an existing Java codebase - Team is Java-fluent without Kotlin experience - Integrating Java-only SDKs or legacy modules - Gradual migration: new Kotlin modules alongside old Java modules > **Java + Kotlin interop is seamless** — you can have both in the same project. New files can be Kotlin while legacy files stay Java. --- ## Project Structure ``` app/src/main/java/com/example/app/ ├── MyApp.java # Application class ├── MainActivity.java # Host activity ├── ui/ │ └── home/ │ ├── HomeActivity.java # OR Fragment-based │ ├── HomeFragment.java │ └── HomeAdapter.java ├── viewmodel/ │ └── HomeViewModel.java ├── repository/ │ └── ItemRepository.java ├── data/ │ ├── remote/ │ │ ├── ApiService.java # Retrofit interface │ │ ├── ApiClient.java # OkHttp + Retrofit setup │ │ └── dto/ItemDto.java │ └── local/ │ ├── AppDatabase.java # Room database │ ├── ItemDao.java │ └── entity/ItemEntity.java ├── model/ │ └── Item.java # Domain model └── di/ # Manual DI or Hilt ``` --- ## ViewModel (Java + LiveData) ```java public class HomeViewModel extends ViewModel { private final MutableLiveData>> _uiState = new MutableLiveData<>(UiState.loading()); public LiveData>> uiState = _uiState; private final ItemRepository repository; private final ExecutorService executor = Executors.newSingleThreadExecutor(); // Constructor injection (Hilt or manual) public HomeViewModel(ItemRepository repository) { this.repository = repository; loadItems(); } public void loadItems() { _uiState.setValue(UiState.loading()); executor.execute(() -> { try { List items = repository.getItems(); _uiState.postValue(UiState.success(items)); } catch (Exception e) { _uiState.postValue(UiState.error(e.getMessage())); } }); } @Override protected void onCleared() { super.onCleared(); executor.shutdown(); } } ``` --- ## UiState Wrapper ```java public class UiState { public enum Status { LOADING, SUCCESS, ERROR } public final Status status; public final T data; public final String errorMessage; private UiState(Status status, T data, String errorMessage) { this.status = status; this.data = data; this.errorMessage = errorMessage; } public static UiState loading() { return new UiState<>(Status.LOADING, null, null); } public static UiState success(T data) { return new UiState<>(Status.SUCCESS, data, null); } public static UiState error(String message) { return new UiState<>(Status.ERROR, null, message); } public boolean isLoading() { return status == Status.LOADING; } public boolean isSuccess() { return status == Status.SUCCESS; } public boolean isError() { return status == Status.ERROR; } } ``` --- ## Fragment Observing ViewModel ```java public class HomeFragment extends Fragment { private HomeViewModel viewModel; private FragmentHomeBinding binding; // ViewBinding @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentHomeBinding.inflate(inflater, container, false); return binding.getRoot(); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); viewModel = new ViewModelProvider(this, new HomeViewModelFactory(new ItemRepository(requireContext()))) .get(HomeViewModel.class); viewModel.uiState.observe(getViewLifecycleOwner(), state -> { binding.progressBar.setVisibility(state.isLoading() ? View.VISIBLE : View.GONE); binding.recyclerView.setVisibility(state.isSuccess() ? View.VISIBLE : View.GONE); binding.errorView.setVisibility(state.isError() ? View.VISIBLE : View.GONE); if (state.isSuccess()) { adapter.submitList(state.data); } if (state.isError()) { binding.errorText.setText(state.errorMessage); } }); binding.retryButton.setOnClickListener(v -> viewModel.loadItems()); } @Override public void onDestroyView() { super.onDestroyView(); binding = null; // CRITICAL — avoid memory leak } } ``` --- ## Room Database (Java) ```java // Entity @Entity(tableName = "items") public class ItemEntity { @PrimaryKey @NonNull public String id; public String title; public long updatedAt; public ItemEntity(@NonNull String id, String title, long updatedAt) { this.id = id; this.title = title; this.updatedAt = updatedAt; } } // DAO @Dao public interface ItemDao { @Query("SELECT * FROM items ORDER BY updatedAt DESC") LiveData> observeAll(); @Query("SELECT * FROM items ORDER BY updatedAt DESC") List getAll(); // blocking — call off main thread @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List items); @Query("DELETE FROM items") void deleteAll(); } // Database @Database(entities = {ItemEntity.class}, version = 1, exportSchema = true) public abstract class AppDatabase extends RoomDatabase { private static volatile AppDatabase INSTANCE; public abstract ItemDao itemDao(); public static AppDatabase getInstance(Context context) { if (INSTANCE == null) { synchronized (AppDatabase.class) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder( context.getApplicationContext(), AppDatabase.class, "app_database" ).build(); } } } return INSTANCE; } } ``` --- ## Retrofit API Client (Java) ```java // Interface public interface ApiService { @GET("items") Call> getItems(); @GET("items/{id}") Call getItemById(@Path("id") String id); @POST("items") Call createItem(@Body ItemDto item); } // Client setup public class ApiClient { private static final String BASE_URL = BuildConfig.API_BASE_URL; private static ApiService INSTANCE; public static ApiService getInstance() { if (INSTANCE == null) { OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .addInterceptor(new AuthInterceptor()) .addInterceptor(new HttpLoggingInterceptor() .setLevel(BuildConfig.DEBUG ? HttpLoggingInterceptor.Level.BODY : HttpLoggingInterceptor.Level.NONE)) .build(); Retrofit retrofit = new Retrofit.Builder() .baseUrl(BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); INSTANCE = retrofit.create(ApiService.class); } return INSTANCE; } } // Auth interceptor public class AuthInterceptor implements Interceptor { @NonNull @Override public Response intercept(@NonNull Chain chain) throws IOException { String token = TokenStorage.getInstance().getToken(); Request request = chain.request().newBuilder() .addHeader("Authorization", "Bearer " + token) .build(); return chain.proceed(request); } } ``` --- ## Repository (Java) ```java public class ItemRepository { private final ItemDao itemDao; private final ApiService apiService; private final ExecutorService executor = Executors.newSingleThreadExecutor(); public ItemRepository(Context context) { AppDatabase db = AppDatabase.getInstance(context); this.itemDao = db.itemDao(); this.apiService = ApiClient.getInstance(); } // Synchronous fetch for ViewModel executor public List getItems() throws Exception { Response> response = apiService.getItems().execute(); if (response.isSuccessful() && response.body() != null) { return response.body().stream() .map(ItemMapper::toDomain) .collect(Collectors.toList()); } else { throw new IOException("HTTP " + response.code()); } } // Observe cached data (returns LiveData — auto updates UI) public LiveData> observeItems() { return Transformations.map(itemDao.observeAll(), entities -> entities.stream().map(ItemMapper::toDomain).collect(Collectors.toList()) ); } // Refresh from network (call from background thread or executor) public void refreshItems(Callback callback) { executor.execute(() -> { try { Response> response = apiService.getItems().execute(); if (response.isSuccessful() && response.body() != null) { List entities = response.body().stream() .map(ItemMapper::toEntity) .collect(Collectors.toList()); itemDao.deleteAll(); itemDao.insertAll(entities); callback.onSuccess(null); } else { callback.onError(new IOException("HTTP " + response.code())); } } catch (IOException e) { callback.onError(e); } }); } public interface Callback { void onSuccess(T result); void onError(Exception e); } } ``` --- ## RecyclerView Adapter (Java) ```java public class ItemAdapter extends ListAdapter { private final OnItemClickListener listener; public interface OnItemClickListener { void onItemClick(Item item); } public ItemAdapter(OnItemClickListener listener) { super(new DiffUtil.ItemCallback() { @Override public boolean areItemsTheSame(@NonNull Item a, @NonNull Item b) { return a.getId().equals(b.getId()); } @Override public boolean areContentsTheSame(@NonNull Item a, @NonNull Item b) { return a.equals(b); } }); this.listener = listener; } @NonNull @Override public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { ItemRowBinding binding = ItemRowBinding.inflate( LayoutInflater.from(parent.getContext()), parent, false); return new ItemViewHolder(binding); } @Override public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) { holder.bind(getItem(position), listener); } static class ItemViewHolder extends RecyclerView.ViewHolder { private final ItemRowBinding binding; ItemViewHolder(ItemRowBinding binding) { super(binding.getRoot()); this.binding = binding; } void bind(Item item, OnItemClickListener listener) { binding.titleText.setText(item.getTitle()); binding.getRoot().setOnClickListener(v -> listener.onItemClick(item)); } } } ``` --- ## XML Layout Best Practices (Java projects) ```xml ``` - Always use **ViewBinding** (not `findViewById`, not DataBinding for simple cases) - Enable in `build.gradle.kts`: `viewBinding { enable = true }` - Null `binding` in `onDestroyView()` to prevent Fragment memory leaks --- ## Error Handling (Java) ```java // Checked exceptions: always handle explicitly public Result> getItemsSafe() { try { Response> response = apiService.getItems().execute(); if (!response.isSuccessful()) { return Result.failure(new HttpException(response)); } List items = Objects.requireNonNull(response.body()) .stream().map(ItemMapper::toDomain).collect(Collectors.toList()); return Result.success(items); } catch (IOException e) { return Result.failure(new NetworkException("Network error", e)); } catch (NullPointerException e) { return Result.failure(new ParseException("Empty response body", e)); } } // Custom exception hierarchy public class AppException extends Exception { public AppException(String message) { super(message); } public AppException(String message, Throwable cause) { super(message, cause); } } public class NetworkException extends AppException { ... } public class ParseException extends AppException { ... } public class AuthException extends AppException { ... } ``` --- ## Hilt DI (Java) ```java // Application @HiltAndroidApp public class MyApp extends Application {} // Activity / Fragment — annotate for injection @AndroidEntryPoint public class HomeFragment extends Fragment { @Inject ItemRepository repository; // injected by Hilt } // ViewModel @HiltViewModel public class HomeViewModel extends ViewModel { private final ItemRepository repository; @Inject public HomeViewModel(ItemRepository repository) { this.repository = repository; } } // Module @Module @InstallIn(SingletonComponent.class) public class DatabaseModule { @Provides @Singleton public AppDatabase provideDatabase(@ApplicationContext Context context) { return AppDatabase.getInstance(context); } @Provides public ItemDao provideItemDao(AppDatabase db) { return db.itemDao(); } } ``` --- ## Unit Testing (Java) ```java @ExtendWith(MockitoExtension.class) class HomeViewModelTest { @Mock ItemRepository mockRepository; HomeViewModel viewModel; @BeforeEach void setup() { viewModel = new HomeViewModel(mockRepository); } @Test void loadItems_success_emitsSuccessState() throws Exception { List items = Arrays.asList(new Item("1", "Test")); when(mockRepository.getItems()).thenReturn(items); viewModel.loadItems(); // Wait for executor — use CountDownLatch or InstantExecutorRule UiState> state = viewModel.uiState.getValue(); assertNotNull(state); assertTrue(state.isSuccess()); assertEquals(items, state.data); } @Test void loadItems_failure_emitsErrorState() throws Exception { when(mockRepository.getItems()).thenThrow(new IOException("Network error")); viewModel.loadItems(); UiState> state = viewModel.uiState.getValue(); assertNotNull(state); assertTrue(state.isError()); } } ``` --- ## Java → Kotlin Migration Path When migrating a Java project to Kotlin incrementally: 1. **New files in Kotlin** — Java and Kotlin coexist seamlessly 2. **Convert utilities first** — `@JvmStatic`, `@JvmField` for interop 3. **Convert data models** — Java POJOs → Kotlin `data class` 4. **Convert DAOs and Repositories** — add `suspend` + `Flow` 5. **Convert ViewModels last** — swap `LiveData` + `MutableLiveData` for `StateFlow` 6. **Convert Activities/Fragments** — migrate to Compose screen by screen 7. Annotate Kotlin with `@JvmOverloads`, `@JvmName` where Java callers exist ```kotlin // Kotlin data class replacing a Java POJO data class Item( val id: String, val title: String, val updatedAt: Long = System.currentTimeMillis() ) // Kotlin extension to consume Java LiveData from Kotlin cleanly fun LiveData.observeNonNull(owner: LifecycleOwner, observer: (T) -> Unit) { observe(owner) { it?.let(observer) } } ```