既存のアプリにJetpack Composeを適用する
はじめに
以前、Jetpack Composeを使ってみる という記事を書きました。
その続編です。
目的
タイトル通りですが、既にあるアプリにJetpack Compseを適用することを目的とします。
サンプルコードの紹介
Android公式のJetpack ComposeのサンプルコードにJetNewsがあります。
Android Stduioでのサンプルコードの取得方法はこちら
JetNewsを参考に既存のアプリにJetpack Compseを適用しましょう。
準備
まずは既存のアプリを準備しましょう。
※ Android 4.2 Canary5を使用しています。
Android Studioのメニューから新しいプロジェクトを作成してください。
その際、Basic Activityを選択してください。
出来上がったプロジェクトは以下のような画面構成になります。
navi_graph.xml
のスクリーンショットです。
FirstFragmentでNEXTボタンを押下すると、SecondFragmentに遷移し、
SecondFragmentでPREVIOUSでFirstFragmentに遷移します。
このアプリにJetpack Composeを適用していきます。
Jetpack Composeライブラリを適用する
まずはbuild.gradleの設定です。
android {
compileSdkVersion 30
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "opst.co.jp.jetpackfragmentsample"
minSdkVersion 23
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
// Enables Jetpack Compose for this module
compose true
}
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
composeOptions {
kotlinCompilerExtensionVersion "0.1.0-dev14"
}
}
dependencies {
...
implementation 'androidx.ui:ui-core:0.1.0-dev14'
implementation 'androidx.ui:ui-tooling:0.1.0-dev14'
implementation 'androidx.ui:ui-layout:0.1.0-dev14'
implementation 'androidx.ui:ui-material:0.1.0-dev14'
...
}
各FragmentをComposableで作成する
FirstFragment、SecondFragmentをそれぞれComposableで置き換えましょう。
AppBarのアイコンと、FloatingActionButtonのアイコンが違うのはここでは放置します。
FirstFragmentは以下のようにします。
@Composable
fun FirstScreen(scaffoldState: ScaffoldState = remember { ScaffoldState()}) {
Scaffold(
scaffoldState = scaffoldState,
floatingActionButton = {
Button(onClick = {} ) {
}
},
drawerContent = {
},
topBar = {
TopAppBar(
title = { Text(text = "jetpackFragmentSample") }
)
},
bodyContent = { innerPadding ->
FirstScreenContent()
}
)
}
@Composable
fun FirstScreenContent() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalGravity = Alignment.CenterHorizontally
) {
Text(
text = "Hello first fragment",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontSize = TextUnit.Companion.Sp(14)
)
Button(
onClick = {}) {
Text(text = "NEXT")
}
}
}
@Preview
@Composable
fun FirstScreenPreview() {
FirstScreen()
}
SecondFragmentは以下のようにします。
@Composable
fun SecondScreen(scaffoldState: ScaffoldState = remember { ScaffoldState() }) {
Scaffold(
scaffoldState = scaffoldState,
floatingActionButton = {
Button(onClick = {}) {
}
},
drawerContent = {
},
topBar = {
TopAppBar(
title = { Text(text = "jetpackFragmentSample") }
)
},
bodyContent = { innerPadding ->
SecondScreenContent()
}
)
}
@Composable
fun SecondScreenContent() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalGravity = Alignment.CenterHorizontally
) {
Text(
text = "",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center)
Button(
onClick = {}) {
Text(text = "PREVIOUS")
}
}
}
@Preview
@Composable
fun SecondScreenPreview() {
SecondScreen()
}
これで画面はできましたが、まだ遷移はできません。
Fragmentの遷移
既存アプリでは、FirstFragmentとSecondFragment間の遷移はNavigation Componentが使われています。
JetNews Sampleでは画面の遷移をComposable Functionを差し替えていくことで実現しています。
以下がその部分のコードです。
navigationViewModel.currentScreen が変更されると、HomeScreen、InterestsScreen、ArticleScreenにそれぞれ遷移します。
class MainActivity : AppCompatActivity() {
val navigationViewModel by viewModels<NavigationViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val appContainer = (application as JetnewsApplication).container
setContent {
JetnewsApp(appContainer, navigationViewModel)
}
}
override fun onBackPressed() {
if (!navigationViewModel.onBack()) {
super.onBackPressed()
}
}
}
@Composable
fun JetnewsApp(
appContainer: AppContainer,
navigationViewModel: NavigationViewModel
) {
JetnewsTheme {
AppContent(
navigationViewModel = navigationViewModel,
interestsRepository = appContainer.interestsRepository,
postsRepository = appContainer.postsRepository
)
}
}
@Composable
private fun AppContent(
navigationViewModel: NavigationViewModel,
postsRepository: PostsRepository,
interestsRepository: InterestsRepository
) {
Crossfade(navigationViewModel.currentScreen) { screen ->
Surface(color = MaterialTheme.colors.background) {
when (screen) {
is Screen.Home -> HomeScreen(
navigateTo = navigationViewModel::navigateTo,
postsRepository = postsRepository
)
is Screen.Interests -> InterestsScreen(
navigateTo = navigationViewModel::navigateTo,
interestsRepository = interestsRepository
)
is Screen.Article -> ArticleScreen(
postId = screen.postId,
postsRepository = postsRepository,
onBack = { navigationViewModel.onBack() }
)
}
}
}
}
このソースを参考にしながら、画面遷移を作成します。
まず、JetNewsSampleのnavigation.ktを流用して以下のようにします。
また、SavedStateHandleUtils.ktはそのまま流用します。
※ 見辛くなるのでコメントは消してあります
enum class ScreenName { FIRST, SECOND}
sealed class Screen(val id: ScreenName) {
object First : Screen(ScreenName.FIRST)
object Second : Screen(ScreenName.SECOND)
}
private const val SIS_SCREEN = "sis_screen"
private const val SIS_NAME = "screen_name"
private fun Screen.toBundle(): Bundle {
return bundleOf(SIS_NAME to id.name)
}
private fun Bundle.toScreen(): Screen {
val screenName = ScreenName.valueOf(getStringOrThrow(SIS_NAME))
return when (screenName) {
ScreenName.FIRST -> Screen.First
ScreenName.SECOND -> Screen.Second
}
}
private fun Bundle.getStringOrThrow(key: String) =
requireNotNull(getString(key)) { "Missing key '$key' in $this" }
class NavigationViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
var currentScreen: Screen by savedStateHandle.getMutableStateOf<Screen>(
key = SIS_SCREEN,
default = Screen.First,
save = { it.toBundle() },
restore = { it.toScreen() }
)
private set // limit the writes to only inside this class.
@MainThread
fun onBack(): Boolean {
val wasHandled = currentScreen != Screen.First
currentScreen = Screen.First
return wasHandled
}
@MainThread
fun navigateTo(screen: Screen) {
currentScreen = screen
}
}
画面遷移部分を実装する
currentScreenが変更されたら画面を切り替える部分を実装しましょう。
FirstScreen,SecondScreenを実際に切り替えるところは以下のようになります。
@Composable
fun jetpackFragmentSample(navigationViewModel: NavigationViewModel) {
MaterialTheme {
Surface(color = MaterialTheme.colors.background) {
when (navigationViewModel.currentScreen) {
Screen.First -> FirstScreen(navigateTo = navigationViewModel::navigateTo)
Screen.Second -> SecondScreen(navigateTo = navigationViewModel::navigateTo)
}
}
}
}
これに合わせて、FirstScreen,SecondScreenのボタンが押されたら、currentScreenを切り替える処理を追加しましょう。
@Composable
fun FirstScreen(
navigateTo: (Screen) -> Unit,
scaffoldState: ScaffoldState = remember { ScaffoldState() }
) {
Scaffold(
scaffoldState = scaffoldState,
floatingActionButton = {
Button(onClick = {}) {
}
},
drawerContent = {
},
topBar = {
TopAppBar(
title = { Text(text = "jetpackFragmentSample") }
)
},
bodyContent = { innerPadding ->
FirstScreenContent(navigateTo)
}
)
}
@Composable
fun FirstScreenContent(
navigateTo: (Screen) -> Unit
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalGravity = Alignment.CenterHorizontally
) {
Text(
text = "Hello first fragment",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontSize = TextUnit.Companion.Sp(14)
)
Button(
onClick = {
navigateTo(Screen.Second)
}) {
Text(text = "NEXT")
}
}
}
@Preview
@Composable
fun FirstScreenPreview() {
FirstScreen(navigateTo = { })
}
@Composable
fun SecondScreen(
navigateTo: (Screen) -> Unit,
scaffoldState: ScaffoldState = remember { ScaffoldState() }
) {
Scaffold(
scaffoldState = scaffoldState,
floatingActionButton = {
Button(onClick = {}) {
}
},
drawerContent = {
},
topBar = {
TopAppBar(
title = { Text(text = "jetpackFragmentSample") }
)
},
bodyContent = { innerPadding ->
SecondScreenContent(navigateTo = navigateTo)
}
)
}
@Composable
fun SecondScreenContent(
navigateTo: (Screen) -> Unit
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalGravity = Alignment.CenterHorizontally
) {
Text(
text = "",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
Button(
onClick = {
navigateTo(Screen.First)
}) {
Text(text = "PREVIOUS")
}
}
}
@Preview
@Composable
fun SecondScreenPreview() {
SecondScreen(navigateTo = {})
}
MainActivityからsetContent
あとはMainActivityからsetContentするだけです。
class MainActivity : AppCompatActivity() {
val navigationViewModel by viewModels<NavigationViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
jetpackFragmentSample(navigationViewModel)
}
}
}
これでボタンを押下するたびに画面が切り替わるようになります。
課題
以下の様な課題があるかと思います。
- 画面遷移部分はNavigation Componentと親和性がない
- バックスタックの管理など、画面遷移の状態管理をしなければならない
もしかすると、各FragmentでsetContentして画面遷移はNavigation Componentに任せるのが正解かもしれません。
また何かわかったら書きます。