Multi Module Jetpack Compose Recomposition Skippable Issue


こんにちは!このプレゼンテーションでは、マルチモジュールでコンポーネントを使用する際の注意点について説明します.
https://qiita.com/takahirom/items/6907e810d3661e19cfcf
このメッセージングが表示され、作成されます.
最近、ますます多くのマルチモジュールプロジェクトがクライアントアーキテクチャを構成しています.また、プロポーズを導入する項目も多く増えています.
クライアント・アーキテクチャを構成する場合、通常はドメイン・レイヤでモデルを構成し、Presenterレイヤでこれらのモデルを使用します.
ただし、プレゼンテーションレイヤでリクエストを使用する場合は、注意すべき事項が分からない場合があります.
簡単に言えば、複合で「再結合」が発生した場合、ドメインレイヤで使用されるモデルがパラメータとして使用される場合、その組合せのスキップは発生しません.
このような状況を再現するために、以下のようにプロジェクトを構成しました.

Domain Layer

data class User(
	val name: String
)

interface UserRepository {
	fun observeUser(): Flow<User>
	suspend fun setUser()
}

class SetUserUseCase @Inject constructor(
	private val userRepository: UserRepository
) {
	suspend operator fun invoke() = userRepository.setUser()
}

class ObserveUserUseCase @Inject constructor(
	private val userRepository: UserRepository
) {
	operator fun invoke() = userRepository.observeUser()
}

Data Layer

@Module
@InstallIn(SingletonComponent::class)
interface RepositoryModule {

	@Binds
	fun bindUserRepository(userRepository: UserRepositoryImpl): UserRepository
}

@Singleton
class UserApiExecutor @Inject constructor() {

	private val _user: MutableSharedFlow<User> = MutableSharedFlow()
	val user: SharedFlow<User> get() = _user

	var cnt = 2
	suspend fun setUser() {
		_user.emit(User(cnt++.toString()))
	}
}

class UserRepositoryImpl @Inject constructor(
	private val userApiExecutor: UserApiExecutor
) : UserRepository {

	override fun observeUser(): Flow<User> = userApiExecutor.user

	override suspend fun setUser() = userApiExecutor.setUser()
}

Presentation Layer

@HiltAndroidApp
class ModelTestApplication : Application()

@Stable
data class UserUiState(
	val user: User = User("1")
)

@HiltViewModel
class MainViewModel @Inject constructor(
	observeUserUseCase: ObserveUserUseCase,
	private val setUserUseCase: SetUserUseCase
) : ViewModel() {

	val user = observeUserUseCase()
		.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), User("1"))

	val wrappedUser = observeUserUseCase()
		.map(::UserUiState)
		.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), UserUiState())

	private val _otherState: MutableStateFlow<Int> = MutableStateFlow(0)
	val otherState: StateFlow<Int> = _otherState

	fun onButtonClick() {
		viewModelScope.launch {
			setUserUseCase()
		}
	}

	fun onOtherAction() {
		_otherState.update {
			it + 1
		}
	}
}

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)

		setContent {
			val viewModel: MainViewModel = viewModel()
			val otherState by viewModel.otherState.collectAsState()
			val userState by viewModel.wrappedUser.collectAsState()
			val user by viewModel.user.collectAsState()

			Column(
				modifier = Modifier.fillMaxSize(),
				verticalArrangement = Arrangement.SpaceBetween
			) {
				UserProfile(user)
				WrappedUserProfile(userUiState = userState)
				Button(viewModel::onButtonClick) {
					Text(text = "Set User")
				}
				OtherContent(otherState = otherState)
				Button(viewModel::onOtherAction) {
					Text(text = "Other Action")
				}
			}
		}
	}
}

@Composable
fun UserProfile(user: User) {
	println("User Profile Composable")
	Text(text = user.name)
}

@Composable
fun WrappedUserProfile(userUiState: UserUiState) {
	println("Wrapped User Profile Composable")
	Text(text = userUiState.user.name)
}

@Composable
fun OtherContent(otherState: Int) {
	println("Other Content Composable")
	Text(text = otherState.toString())
}

イニシアチブ


Domain Layerで構成されたモデルに直接書き込むユーザーと@Stable Annotationを適用するwrappedUser、およびPresenterレイヤで構成されたotherStateの3つの変数を宣言して使用します.
2つのボタンを作成します.1つはuser値を変更し、user値とwrappedUser値を変更し、もう1つはotherState値を変更します.
通常、ユーザー値が変更された場合、「コンテンツ」のみが予想通りに合成され、「プロファイル」、「WrappedUserProfile」および「その他のステータス」の値が変更された場合、「コンテンツ」は合成されます.
結果を見てみましょう.

結果ログ


最初のコンポーネント


2022-04-07 16:04:18.866 8922-8922/com.example.multimodulecomposemodeltest I/System.out: User Profile Composable
2022-04-07 16:04:18.873 8922-8922/com.example.multimodulecomposemodeltest I/System.out: Wrapped User Profile Composable
2022-04-07 16:04:18.893 8922-8922/com.example.multimodulecomposemodeltest I/System.out: Other Content Composable

ユーザー値の変更


2022-04-07 16:04:28.696 8922-8922/com.example.multimodulecomposemodeltest I/System.out: User Profile Composable
2022-04-07 16:04:28.698 8922-8922/com.example.multimodulecomposemodeltest I/System.out: Wrapped User Profile Composable

その他のステータス値の変更


2022-04-07 16:06:15.791 9474-9474/com.example.multimodulecomposemodeltest I/System.out: User Profile Composable
2022-04-07 16:06:15.793 9474-9474/com.example.multimodulecomposemodeltest I/System.out: Other Content Composable
結果として,シーンでは予想通りに合成されず,ドメイン層モデルであるユーザはSkipにならずに合成を実行し続けることが分かる.
トランスファにも良い解決策があります.
@Stable Annotationを使用してこの問題を解決する方法と、Compose Runtime Denependencyをモデルを使用するモジュールに適用します.
ただし、Pure JavaおよびKotlinライブラリとして通常ドメイン層が使用されるため、Android Frameworkから返されるComponent Runtime Dependencyは適用できません.
したがって,@Stable Annotationで再結合する場合にはSkipの期待値が適切である.

References


https://qiita.com/takahirom/items/6907e810d3661e19cfcf