Skip to content

Commit

Permalink
Fix part of #5344, Fix Part of #5422 and Fix Part of #5012: Implement…
Browse files Browse the repository at this point in the history
… Classroom List Screen with Jetpack Compose (#5437)

<!-- READ ME FIRST: Please fill in the explanation section below and
check off every point from the Essential Checklist! -->
## Explanation
<!--
- Explain what your PR does. If this PR fixes an existing bug, please
include
- "Fixes #bugnum:" in the explanation so that GitHub can auto-close the
issue
  - when this PR is merged.
  -->

Fixes part of #5344
Fixes part of #5422
Fixes part of #5012

This PR introduces the Classroom List Screen, which will replace the
existing Home Screen. The new screen features a classroom carousel that
remains sticky when the screen is scrolled. Various approaches were
considered for implementing the sticky header, as detailed in [Decision
3: How to implement the sticky classroom
carousel?](https://docs.google.com/document/d/1wGnoDS-8zHS5QSHufvTf9SXKi8N3lodirs51Xnehdvo/edit#heading=h.fv51u13soqp4).
Ultimately, it was decided to use the `stickyHeader` component of
Jetpack Compose.

- **Introduce Jetpack Compose** - Sets the groundwork for migrating the
codebase to Jetpack Compose. The Classroom List Screen uses a
`ComposeView` to host the composable components. Appropriate tests have
been set up to verify the functionality.
- **Migrate away from `onActivityResult`** - Deprecates the use of
`onActivityResult` and transitions to using `ActivityResultContracts`
for handling activity results.
- **Initiate Deprecation of Android KitKat** - The selected versions of
Jetpack Compose dependencies require the `minSdkVersion` to be atleast
21. This PR initiates the process of deprecating support for Android 19
(KitKat).


https://github.com/oppia/oppia-android/assets/84731134/2a2145e5-5bd0-4d80-9741-ff6baf20fd12

## Screenshots
### Phone Light Mode
|Potrait|Landscape|
|--|--|

|![image](https://github.com/oppia/oppia-android/assets/84731134/367fe033-833a-4c44-b9ae-05a79c872271)|![image](https://github.com/oppia/oppia-android/assets/84731134/771f3681-884b-45d4-91af-880b775a5937)|

|![image](https://github.com/oppia/oppia-android/assets/84731134/921a136c-d84c-479f-a7e6-f40aa55e9b5b)|![image](https://github.com/oppia/oppia-android/assets/84731134/8783f5f8-3b09-4899-b286-09f1d9cfae2f)|

### Phone Dark Mode
|Potrait|Landscape|
|--|--|

|![image](https://github.com/oppia/oppia-android/assets/84731134/2fd50b98-cd5e-45bb-aeff-8690886ddf28)|![image](https://github.com/oppia/oppia-android/assets/84731134/64053fd1-0c22-414a-9cbf-b6fcb615fe38)|

|![image](https://github.com/oppia/oppia-android/assets/84731134/2d2160ba-c886-48c7-8832-f1b8327e4daa)|![image](https://github.com/oppia/oppia-android/assets/84731134/7c3974a0-0c4a-4de2-a39b-2239f27bdd64)|

### Tablet Light Mode
|Potrait|Landscape|
|--|--|

|![image](https://github.com/oppia/oppia-android/assets/84731134/459907ce-4bdf-44dd-a84b-e9e50baaa53b)|![image](https://github.com/oppia/oppia-android/assets/84731134/fc3076de-35bf-493e-8b88-e74658285f61)|

||![image](https://github.com/oppia/oppia-android/assets/84731134/34d34a58-e173-488b-9ba4-5de28c2d1342)|

### Tablet Dark Mode
|Potrait|Landscape|
|--|--|

|![image](https://github.com/oppia/oppia-android/assets/84731134/8eff1354-9faa-4717-93df-ae643db0131b)|![image](https://github.com/oppia/oppia-android/assets/84731134/1fe8232d-399f-4268-a889-854e9680529c)|

||![image](https://github.com/oppia/oppia-android/assets/84731134/c8b09dfe-00d9-4d1b-a991-dd2c049d4de1)|

## Essential Checklist
<!-- Please tick the relevant boxes by putting an "x" in them. -->
- [x] The PR title and explanation each start with "Fix #bugnum: " (If
this PR fixes part of an issue, prefix the title with "Fix part of
#bugnum: ...".)
- [x] Any changes to
[scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets)
files have their rationale included in the PR explanation.
- [x] The PR follows the [style
guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [x] The PR does not contain any unnecessary code changes from Android
Studio
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)).
- [x] The PR is made from a branch that's **not** called "develop" and
is up-to-date with "develop".
- [x] The PR is **assigned** to the appropriate reviewers
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)).

## For UI-specific PRs only
<!-- Delete these section if this PR does not include UI-related
changes. -->
If your PR includes UI-related changes, then:
- Add screenshots for portrait/landscape for both a tablet & phone of
the before & after UI changes
- For the screenshots above, include both English and pseudo-localized
(RTL) screenshots (see [RTL
guide](https://github.com/oppia/oppia-android/wiki/RTL-Guidelines))
- Add a video showing the full UX flow with a screen reader enabled (see
[accessibility
guide](https://github.com/oppia/oppia-android/wiki/Accessibility-A11y-Guide))
- For PRs introducing new UI elements or color changes, both light and
dark mode screenshots must be included
- Add a screenshot demonstrating that you ran affected Espresso tests
locally & that they're passing

---------

Co-authored-by: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com>
  • Loading branch information
theMr17 and adhiamboperes committed Jun 30, 2024
1 parent f53c7e5 commit 103f5a8
Show file tree
Hide file tree
Showing 69 changed files with 4,218 additions and 225 deletions.
2 changes: 1 addition & 1 deletion BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ package_group(
{
"flavor": "oppia_kitkat",
"main_dex_list": "//:config/kitkat_main_dex_class_list.txt",
"min_sdk_version": 19,
"min_sdk_version": 21,
"multidex": "manual_main_dex",
"target_sdk_version": 33,
},
Expand Down
17 changes: 15 additions & 2 deletions app/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@ android_library(
"//model/src/main/proto:thumbnail_java_proto_lite",
"//model/src/main/proto:version_java_proto_lite",
"//third_party:androidx_annotation_annotation",
"//third_party:androidx_compose_ui_ui",
"//third_party:androidx_constraintlayout_constraintlayout",
"//third_party:androidx_core_core",
"//third_party:androidx_databinding_databinding-adapters",
Expand Down Expand Up @@ -763,6 +764,7 @@ kt_android_library(
custom_package = "org.oppia.android.app.ui",
enable_data_binding = 1,
manifest = "src/main/AppAndroidManifest.xml",
plugins = ["//tools/kotlin:jetpack_compose_compiler_plugin"],
visibility = ["//visibility:public"],
deps = [
":binding_adapters",
Expand All @@ -787,6 +789,15 @@ kt_android_library(
"//domain/src/main/java/org/oppia/android/domain/survey:gating_controller",
"//domain/src/main/java/org/oppia/android/domain/survey:survey_controller",
"//model/src/main/proto:arguments_java_proto_lite",
"//third_party:androidx_activity_activity-compose",
"//third_party:androidx_annotation_annotation",
"//third_party:androidx_appcompat_appcompat",
"//third_party:androidx_compose_foundation_foundation",
"//third_party:androidx_compose_foundation_foundation-layout",
"//third_party:androidx_compose_material_material",
"//third_party:androidx_compose_runtime_runtime",
"//third_party:androidx_compose_ui_ui",
"//third_party:androidx_core_core-ktx",
"//third_party:androidx_databinding_databinding-adapters",
"//third_party:androidx_databinding_databinding-common",
"//third_party:androidx_databinding_databinding-runtime",
Expand All @@ -800,6 +811,7 @@ kt_android_library(
"//third_party:com_github_takusemba_spotlight",
"//third_party:com_google_android_flexbox_flexbox",
"//third_party:javax_annotation_javax_annotation-api_jar",
"//third_party:org_jetbrains_kotlin_kotlin-stdlib-jdk8_jar",
"//utility",
"//utility/src/main/java/org/oppia/android/util/extensions:bundle_extensions",
"//utility/src/main/java/org/oppia/android/util/parser/image:image_loader",
Expand Down Expand Up @@ -898,6 +910,7 @@ TEST_DEPS = [
"//testing/src/main/java/org/oppia/android/testing/threading:test_module",
"//testing/src/main/java/org/oppia/android/testing/time:test_module",
"//third_party:androidx_annotation_annotation",
"//third_party:androidx_compose_ui_ui-test-junit4",
"//third_party:androidx_core_core",
"//third_party:androidx_databinding_databinding-adapters",
"//third_party:androidx_databinding_databinding-common",
Expand Down Expand Up @@ -956,7 +969,7 @@ MIGRATED_TESTS = [
filtered_tests = MIGRATED_TESTS,
manifest_values = {
"applicationId": "org.oppia.android",
"minSdkVersion": "19",
"minSdkVersion": "21",
"targetSdkVersion": "30",
"versionCode": "0",
"versionName": "0.1-alpha",
Expand All @@ -972,7 +985,7 @@ MIGRATED_TESTS = [
filtered_tests = MIGRATED_TESTS,
manifest_values = {
"applicationId": "org.oppia.android",
"minSdkVersion": "19",
"minSdkVersion": "21",
"targetSdkVersion": "30",
"versionCode": "0",
"versionName": "0.1-alpha",
Expand Down
23 changes: 21 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ apply plugin: 'kotlin-kapt'

android {
compileSdkVersion 33
buildToolsVersion "29.0.2"
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "org.oppia.android"
minSdkVersion 19
minSdkVersion 21
targetSdkVersion 33
versionCode 1
versionName "1.0"
Expand All @@ -30,6 +30,15 @@ android {
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
useIR = true
freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn"]
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
kotlinCompilerVersion kotlin_version
}
buildTypes {
release {
Expand Down Expand Up @@ -147,6 +156,13 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation(
'androidx.appcompat:appcompat:1.0.2',
"androidx.compose.foundation:foundation:$compose_version",
"androidx.compose.foundation:foundation-layout:$compose_version",
"androidx.compose.material:material:$compose_version",
"androidx.compose.runtime:runtime:$compose_version",
"androidx.compose.runtime:runtime-livedata:$compose_version",
"androidx.compose.ui:ui:$compose_version",
"androidx.compose.ui:ui-tooling:$compose_version",
'androidx.constraintlayout:constraintlayout:1.1.3',
'androidx.core:core-ktx:1.0.2',
'androidx.legacy:legacy-support-v4:1.0.0',
Expand All @@ -164,6 +180,7 @@ dependencies {
'com.github.bumptech.glide:glide:4.11.0',
'com.google.android.flexbox:flexbox:3.0.0',
'com.google.android.material:material:1.3.0',
"com.google.android.material:compose-theme-adapter:$compose_version",
'com.google.dagger:dagger:2.41',
'com.google.firebase:firebase-analytics:17.5.0',
'com.google.firebase:firebase-analytics-ktx:17.5.0',
Expand All @@ -189,6 +206,7 @@ dependencies {
'org.glassfish.jaxb:jaxb-runtime:2.3.2',
)
testImplementation(
"androidx.compose.ui:ui-test-junit4:$compose_version",
'androidx.test:core:1.2.0',
'androidx.test.espresso:espresso-contrib:3.1.0',
'androidx.test.espresso:espresso-core:3.2.0',
Expand All @@ -207,6 +225,7 @@ dependencies {
project(":testing"),
)
androidTestImplementation(
"androidx.compose.ui:ui-test-junit4:$compose_version",
'androidx.test:core:1.2.0',
'androidx.test.espresso:espresso-contrib:3.1.0',
'androidx.test.espresso:espresso-core:3.2.0',
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -332,11 +332,19 @@
<activity
android:name=".app.testing.ColorBindingAdaptersTestActivity"
android:theme="@style/OppiaThemeWithoutActionBar" />
<activity
android:name=".app.classroom.ClassroomListActivity"
android:label="@string/classroom_list_activity_title"
android:theme="@style/OppiaThemeWithoutActionBar" />

<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:exported="false"
tools:node="remove" />
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dagger.Subcomponent
import org.oppia.android.app.administratorcontrols.AdministratorControlsActivity
import org.oppia.android.app.administratorcontrols.appversion.AppVersionActivity
import org.oppia.android.app.administratorcontrols.learneranalytics.ProfileAndDeviceIdActivity
import org.oppia.android.app.classroom.ClassroomListActivity
import org.oppia.android.app.completedstorylist.CompletedStoryListActivity
import org.oppia.android.app.devoptions.DeveloperOptionsActivity
import org.oppia.android.app.devoptions.forcenetworktype.ForceNetworkTypeActivity
Expand Down Expand Up @@ -218,4 +219,5 @@ interface ActivityComponentImpl :
fun inject(walkthroughActivity: WalkthroughActivity)
fun inject(surveyActivity: SurveyActivity)
fun inject(colorBindingAdaptersTestActivity: ColorBindingAdaptersTestActivity)
fun inject(classroomListActivity: ClassroomListActivity)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package org.oppia.android.app.classroom

import android.content.Context
import android.content.Intent
import android.os.Bundle
import org.oppia.android.R
import org.oppia.android.app.activity.ActivityComponentImpl
import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity
import org.oppia.android.app.activity.route.ActivityRouter
import org.oppia.android.app.drawer.ExitProfileDialogFragment
import org.oppia.android.app.drawer.TAG_SWITCH_PROFILE_DIALOG
import org.oppia.android.app.home.RouteToRecentlyPlayedListener
import org.oppia.android.app.home.RouteToTopicListener
import org.oppia.android.app.home.RouteToTopicPlayStoryListener
import org.oppia.android.app.model.DestinationScreen
import org.oppia.android.app.model.ExitProfileDialogArguments
import org.oppia.android.app.model.HighlightItem
import org.oppia.android.app.model.ProfileId
import org.oppia.android.app.model.RecentlyPlayedActivityParams
import org.oppia.android.app.model.RecentlyPlayedActivityTitle
import org.oppia.android.app.model.ScreenName.CLASSROOM_LIST_ACTIVITY
import org.oppia.android.app.topic.TopicActivity
import org.oppia.android.app.translation.AppLanguageResourceHandler
import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName
import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import javax.inject.Inject

/** The activity for displaying [ClassroomListFragment]. */
class ClassroomListActivity :
InjectableAutoLocalizedAppCompatActivity(),
RouteToTopicListener,
RouteToTopicPlayStoryListener,
RouteToRecentlyPlayedListener {
@Inject
lateinit var classroomListActivityPresenter: ClassroomListActivityPresenter

@Inject
lateinit var resourceHandler: AppLanguageResourceHandler

@Inject
lateinit var activityRouter: ActivityRouter

private var internalProfileId: Int = -1

companion object {
/** Returns a new [Intent] to route to [ClassroomListActivity] for a specified [profileId]. */
fun createClassroomListActivity(context: Context, profileId: ProfileId?): Intent {
return Intent(context, ClassroomListActivity::class.java).apply {
decorateWithScreenName(CLASSROOM_LIST_ACTIVITY)
profileId?.let { decorateWithUserProfileId(profileId) }
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(activityComponent as ActivityComponentImpl).inject(this)

internalProfileId = intent.extractCurrentUserProfileId().internalId
classroomListActivityPresenter.handleOnCreate()
title = resourceHandler.getStringInLocale(R.string.classroom_list_activity_title)
}

override fun onRestart() {
super.onRestart()
classroomListActivityPresenter.handleOnRestart()
}

override fun onBackPressed() {
val previousFragment =
supportFragmentManager.findFragmentByTag(TAG_SWITCH_PROFILE_DIALOG)
if (previousFragment != null) {
supportFragmentManager.beginTransaction().remove(previousFragment).commitNow()
}
val exitProfileDialogArguments =
ExitProfileDialogArguments
.newBuilder()
.setHighlightItem(HighlightItem.NONE)
.build()
val dialogFragment = ExitProfileDialogFragment
.newInstance(exitProfileDialogArguments = exitProfileDialogArguments)
dialogFragment.showNow(supportFragmentManager, TAG_SWITCH_PROFILE_DIALOG)
}

override fun routeToRecentlyPlayed(recentlyPlayedActivityTitle: RecentlyPlayedActivityTitle) {
val recentlyPlayedActivityParams =
RecentlyPlayedActivityParams
.newBuilder()
.setProfileId(ProfileId.newBuilder().setInternalId(internalProfileId).build())
.setActivityTitle(recentlyPlayedActivityTitle).build()

activityRouter.routeToScreen(
DestinationScreen
.newBuilder()
.setRecentlyPlayedActivityParams(recentlyPlayedActivityParams)
.build()
)
}

override fun routeToTopic(internalProfileId: Int, topicId: String) {
startActivity(TopicActivity.createTopicActivityIntent(this, internalProfileId, topicId))
}

override fun routeToTopicPlayStory(internalProfileId: Int, topicId: String, storyId: String) {
startActivity(
TopicActivity.createTopicPlayStoryActivityIntent(
this,
internalProfileId,
topicId,
storyId
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.oppia.android.app.classroom

import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.drawerlayout.widget.DrawerLayout
import org.oppia.android.R
import org.oppia.android.app.drawer.NavigationDrawerFragment
import javax.inject.Inject

/** Tag for identifying the [ClassroomListFragment] in transactions. */
private const val TAG_CLASSROOM_LIST_FRAGMENT = "CLASSROOM_LIST_FRAGMENT"

/** The presenter for [ClassroomListActivity]. */
class ClassroomListActivityPresenter @Inject constructor(private val activity: AppCompatActivity) {
private var navigationDrawerFragment: NavigationDrawerFragment? = null

/**
* Handles the creation of the activity. Sets the content view, sets up the navigation drawer,
* and adds the [ClassroomListFragment] if it's not already added.
*/
fun handleOnCreate() {
activity.setContentView(R.layout.classroom_list_activity)
setUpNavigationDrawer()
if (getClassroomListFragment() == null) {
activity.supportFragmentManager.beginTransaction().add(
R.id.classroom_list_fragment_placeholder,
ClassroomListFragment(),
TAG_CLASSROOM_LIST_FRAGMENT
).commitNow()
}
}

/** Handles the activity restart. Re-initializes the navigation drawer. */
fun handleOnRestart() {
setUpNavigationDrawer()
}

private fun setUpNavigationDrawer() {
val toolbar = activity.findViewById<View>(R.id.classroom_list_activity_toolbar) as Toolbar
activity.setSupportActionBar(toolbar)
activity.supportActionBar!!.setDisplayShowHomeEnabled(true)
navigationDrawerFragment = activity
.supportFragmentManager
.findFragmentById(
R.id.classroom_list_activity_fragment_navigation_drawer
) as NavigationDrawerFragment
navigationDrawerFragment!!.setUpDrawer(
activity.findViewById<View>(R.id.classroom_list_activity_drawer_layout) as DrawerLayout,
toolbar, R.id.nav_home
)
}

private fun getClassroomListFragment(): ClassroomListFragment? {
return activity.supportFragmentManager.findFragmentById(
R.id.classroom_list_fragment_placeholder
) as ClassroomListFragment?
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.oppia.android.app.classroom

import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.oppia.android.app.fragment.FragmentComponentImpl
import org.oppia.android.app.fragment.InjectableFragment
import org.oppia.android.app.home.classroomlist.ClassroomSummaryClickListener
import org.oppia.android.app.home.topiclist.TopicSummaryClickListener
import org.oppia.android.app.model.ClassroomSummary
import org.oppia.android.app.model.TopicSummary
import javax.inject.Inject

/** Fragment that displays the classroom list screen. */
class ClassroomListFragment :
InjectableFragment(),
TopicSummaryClickListener,
ClassroomSummaryClickListener {
@Inject
lateinit var classroomListFragmentPresenter: ClassroomListFragmentPresenter

override fun onAttach(context: Context) {
super.onAttach(context)
(fragmentComponent as FragmentComponentImpl).inject(this)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return classroomListFragmentPresenter.handleCreateView(inflater, container)
}

override fun onTopicSummaryClicked(topicSummary: TopicSummary) {
classroomListFragmentPresenter.onTopicSummaryClicked(topicSummary)
}

override fun onClassroomSummaryClicked(classroomSummary: ClassroomSummary) {
classroomListFragmentPresenter.onClassroomSummaryClicked(classroomSummary)
}
}
Loading

0 comments on commit 103f5a8

Please sign in to comment.