playbook/antigravity-awesome-skills/plugins/antigravity-awesome-skills-.../skills/android-dev/references/java-android.md

17 KiB

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)

public class HomeViewModel extends ViewModel {

    private final MutableLiveData<UiState<List<Item>>> _uiState =
        new MutableLiveData<>(UiState.loading());

    public LiveData<UiState<List<Item>>> 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<Item> 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

public class UiState<T> {
    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 <T> UiState<T> loading() {
        return new UiState<>(Status.LOADING, null, null);
    }

    public static <T> UiState<T> success(T data) {
        return new UiState<>(Status.SUCCESS, data, null);
    }

    public static <T> UiState<T> 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

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)

// 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<List<ItemEntity>> observeAll();

    @Query("SELECT * FROM items ORDER BY updatedAt DESC")
    List<ItemEntity> getAll(); // blocking — call off main thread

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insertAll(List<ItemEntity> 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)

// Interface
public interface ApiService {
    @GET("items")
    Call<List<ItemDto>> getItems();

    @GET("items/{id}")
    Call<ItemDto> getItemById(@Path("id") String id);

    @POST("items")
    Call<ItemDto> 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)

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<Item> getItems() throws Exception {
        Response<List<ItemDto>> 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<List<Item>> 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<Void> callback) {
        executor.execute(() -> {
            try {
                Response<List<ItemDto>> response = apiService.getItems().execute();
                if (response.isSuccessful() && response.body() != null) {
                    List<ItemEntity> 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<T> {
        void onSuccess(T result);
        void onError(Exception e);
    }
}

RecyclerView Adapter (Java)

public class ItemAdapter extends ListAdapter<Item, ItemAdapter.ItemViewHolder> {

    private final OnItemClickListener listener;

    public interface OnItemClickListener {
        void onItemClick(Item item);
    }

    public ItemAdapter(OnItemClickListener listener) {
        super(new DiffUtil.ItemCallback<Item>() {
            @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)

<!-- Use ConstraintLayout — flat hierarchy = better performance -->
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- Always use ?attr/ tokens from MaterialTheme, never hardcoded colors -->
    <TextView
        android:id="@+id/titleText"
        android:textColor="?attr/colorOnSurface"
        android:textAppearance="?attr/textAppearanceTitleMedium"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
  • 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)

// Checked exceptions: always handle explicitly
public Result<List<Item>> getItemsSafe() {
    try {
        Response<List<ItemDto>> response = apiService.getItems().execute();
        if (!response.isSuccessful()) {
            return Result.failure(new HttpException(response));
        }
        List<Item> 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)

// 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)

@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<Item> items = Arrays.asList(new Item("1", "Test"));
        when(mockRepository.getItems()).thenReturn(items);

        viewModel.loadItems();

        // Wait for executor — use CountDownLatch or InstantExecutorRule
        UiState<List<Item>> 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<List<Item>> 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 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 <T> LiveData<T>.observeNonNull(owner: LifecycleOwner, observer: (T) -> Unit) {
    observe(owner) { it?.let(observer) }
}