Microsoft MVP성태의 닷넷 이야기
Phone: 18. C# MAUI - 안드로이드 플랫폼에서의 Activity 제어 [링크 복사], [링크+제목 복사],
조회: 9403
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 
(연관된 글이 2개 있습니다.)
(시리즈 글이 4개 있습니다.)
Phone: 16. C# MAUI - /Download 등의 공용 디렉터리에 접근하는 방법
; https://www.sysnet.pe.kr/2/0/13631

Phone: 17. C# MAUI - Android 내에 Web 서비스 호스팅
; https://www.sysnet.pe.kr/2/0/13632

Phone: 18. C# MAUI - 안드로이드 플랫폼에서의 Activity 제어
; https://www.sysnet.pe.kr/2/0/13634

Phone: 21. C# MAUI - Android 환경에서의 파일 다운로드(DownloadManager)
; https://www.sysnet.pe.kr/2/0/13640




C# MAUI - 안드로이드 플랫폼에서의 Activity 제어

(이 글은 안드로이드 환경만을 대상으로 합니다.)

MAUI가 기본적으로 XAML을 사용한 UI를 지원하지만 전통적인 Activity를 사용하는 것도 가능합니다. 예를 하나 들어볼까요? 간단하게 Button Click을 하나 만들고,

<Button
        x:Name="btnNewActivity"
        Text="New Activity" 
        SemanticProperties.Hint="Click to show new Activity"
        Clicked="OnNewActivityClicked"
        HorizontalOptions="Fill" />

이벤트 핸들러에서 이런 식으로 Activity를 띄울 수 있습니다.

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    private void OnNewActivityClicked(object sender, EventArgs e)
    {
#if ANDROID
        var currentActivity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
        if (currentActivity == null)
        {
            return;
        }

        var newIntent = new Intent(currentActivity, typeof(NewActivity));
        currentActivity.StartActivity(newIntent);
#endif
    }
}

NewActivity 클래스는 .\Platforms\Android 경로에 대충 이렇게 추가해 두면 됩니다.

// NewActivity.cs

using Android.App;
using Android.OS;

namespace SimplePlayer;

[Activity]
internal class NewActivity : Activity
{
    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
    }
}

당연히 이렇게만 하면 버튼을 클릭했을 때 뜨는 NewActivity 화면은 비어 있게 됩니다.




그나저나, 제가 Xamarin Forms부터 이어져온 .NET MAUI에 대한 이력을 전혀 모르고 있습니다. ^^; 게다가 구조도 아직 눈에 안 들어와서 초보 사용자 수준으로만 다뤄보고 있는데요, 그래서 아직 모르는 것인지, 그런 방법이 없는 것인지 알 수 없지만 현재 .NET MAUI는 XAML을 이용한 폼 구성을 MainActivity에만 종속시키고 있는 듯합니다.

다시 말해, 제가 위에서 만든 NewActivity를 위해 XAML로 UI를 구성할 수 없다는 것입니다. (혹시 방법을 아시는 분은 덧글 부탁드립니다. ^^)

이와 관련해서는 검색해도 자료도 없고, 어느 정도 저렇다고 판단할 수밖에 없는 근거가 있다면, 바로 App.Current에 대한 접근이 static 필드라는 사실입니다.

public static Application? Current { get; set; }

여기서 Current는 .NET MAUI 프로젝트의 App.xaml/App.xaml.cs에 해당하는 인스턴스가 설정되는데요, 그렇다면 오직 하나의 XAML Application 구조만 생성된다는 것이고 그것이 MainActivity가 소유한 Window에만 종속돼 있을 것입니다. 이런 상황에서 다른 Activity에서 (이미 MainActivity가 소유한) XAML을 임의로 가져가서 쓴다는 것이 맞지 않습니다. 애초에 이게 가능하려면 AppShell이나 App이 다중으로 생성될 수 있는 구조여야 하고 그것과 Activity가 연결되었다는 정보가 있어야만 합니다.

하지만, .NET MAUI가 생성하는 MainActivity는 딱히 AppShell/MainPage XAML을 소유한다는 식의 정보는 소스코드 내에 어떤 식으로도 명시돼 있지 않습니다.

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density
    )]
public class MainActivity : MauiAppCompatActivity
{
    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
    }
}




자, 그래서 우리가 새로 생성한 Activity인 경우에는 전통적인 Android 방식의 UI 시스템을 따라야 합니다. 이를 위해 .\Platforms\Android\Resources\layout 디렉터리에 activity_new.axml 파일을 만들어 다음의 내용으로 채웁니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical" >
    <TextView android:id="@+id/txtOutput"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="Hello, world" />
    <Button android:id="@+id/btnSample"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Click" />
</LinearLayout>

그다음 NewActivity에서는 위의 layout을 이렇게 연결하면 됩니다.

[Activity]
internal class NewActivity : Activity
{
    TextView? _txtOutput;
    Android.Widget.Button? _btnSample;

    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        this.SetContentView(Resource.Layout.activity_new);

        _txtOutput = FindViewById<TextView>(Resource.Id.txtOutput);
        _btnSample = FindViewById<Android.Widget.Button>(Resource.Id.btnSample);

        if (_btnSample != null)
        {
            _btnSample.Click += _btnSample_Click;
        }
    }

    private void _btnSample_Click(object? sender, EventArgs e)
    {
        _txtOutput.Text = "Hello, .NET MAUI";
    }
}




그런데 여기서 한 가지 재미있는 점이 있습니다. (XAML과 연결된) MainActivity를 제외하고는, 위와 같이 임의로 생성한 Activity는 MAUI의 Navigation 기능인 Shell.Current.GoToAsync를 사용할 수 없습니다.

일례로, _btnSample_Click에서 다음과 같이 코드를 추가하고,

private async void _btnSample_Click(object? sender, EventArgs e)
{
    _txtOutput.Text = "Hello, .NET MAUI";

    await Shell.Current.GoToAsync($"//{nameof(MainPage)}");
}

실행 후, 버튼을 누르면 MainPage로의 전환이 되지 않습니다. 아래의 질문도 이와 관련된 것인데요,

Navigate to a page in .Net MAUI Anroid Activity
; https://stackoverflow.com/questions/77265785/navigate-to-a-page-in-net-maui-anroid-activity

다시 말해 저렇게 동작할 수는 없습니다. 사실 일면 이해는 됩니다. 위의 구조를 Windows 운영체제로 대입해 보면, WPF Window 내에 Page XAML이 구성돼 있는데 그 인스턴스를 그대로 다른 WPF Window의 Child로 동시에 종속시키는 것이 가능하지 않는 식입니다.

즉, 위의 명령어 실행은 Android의 axml layout UI 화면을 (이미 다른 Activity가 소유한) XAML 화면을 가져와 현재 Activity의 UI로 바꾸려는 시도가 됩니다.

그럼 어떻게 MainPage XAML로 갈 수 있을까요? 간단합니다. Activity 전환을 하면 됩니다. 따라서, Android의 방식에 따라 다음과 같이 Activity를 이동시켜야 합니다.

private void _btnSample_Click(object? sender, EventArgs e)
{
    // await Shell.Current.GoToAsync($"//{nameof(MainPage)}");

    var mainIntent = new Intent(this, typeof(MainActivity));
    StartActivity(mainIntent);
}




그런데, 여기서 또 한 가지 문제가 발생하는데요, 저 코드를 .NET 8/MAUI에서 실행하면 이런 오류 메시지를 보게 될 것입니다.

System.InvalidOperationException: 'Window was already created'

0xFFFFFFFFFFFFFFFF in Android.Runtime.RuntimeNativeMethods.monodroid_debugger_unhandled_exception   C#
0x1A in Android.Runtime.JNINativeWrapper._unhandled_exception at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:13,5 C#
0x1E in Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:126,26  C#
0x12 in Microsoft.Maui.Controls.Window.Microsoft.Maui.IWindow.Created at D:\a\_work\1\s\src\Controls\src\Core\Window\Window.cs:483,5    C#
0xB in Microsoft.Maui.LifecycleEvents.AppHostBuilderExtensions.<>c.<OnConfigureLifeCycle>b__2_0 at D:\a\_work\1\s\src\Core\src\Hosting\LifecycleEvents\AppHostBuilderExtensions.Android.cs:23,6 C#
0xD in Microsoft.Maui.MauiAppCompatActivity. at D:\a\_work\1\s\src\Core\src\Platform\Android\MauiAppCompatActivity.Lifecycle.cs:61,104  C#
0x1F in Microsoft.Maui.LifecycleEvents.LifecycleEventServiceExtensions.InvokeLifecycleEvents<Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnPostCreate> at D:\a\_work\1\s\src\Core\src\LifecycleEvents\LifecycleEventServiceExtensions.cs:31,5 C#
0x40 in Microsoft.Maui.MauiAppCompatActivity.OnPostCreate at D:\a\_work\1\s\src\Core\src\Platform\Android\MauiAppCompatActivity.Lifecycle.cs:61,4   C#
0x11 in Android.App.Activity.n_OnPostCreate_Landroid_os_Bundle_ at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-34/mcw/Android.App.Activity.cs:4488,4 C#
0x9 in Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:125,5    C#

이슈를 보면 .NET 7 이하로는 괜찮았던 것 같은데 .NET 8부터 문제가 된다고 합니다.

MAUI Android build crashes when app is reopened from background. It throws the exception: 'Window was already created.' #18692
; https://github.com/dotnet/maui/issues/18692

위의 문제를 우회하는 방법이 하나 있는데요, 결국 OnPostCreate에서 예외가 발생한 것이기 때문에 아래와 같이 try/catch를 감싸주면 넘어가긴 합니다.

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density
    )]
public class MainActivity : MauiAppCompatActivity
{
    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
    }

    protected override void OnPostCreate(Bundle? savedInstanceState)
    {
        try
        {
            base.OnPostCreate(savedInstanceState);
        }
        catch (Exception e) // 하지만, 내부 코드의 어느 지점에서 예외가 발생했는지에 대해서는 알 수 없으므로 어떤 초기화가 생략됐을지 분석이 필요합니다.
        {
            System.Diagnostics.Trace.WriteLine(e.ToString());
        }
    }
}

위와 같은 현상이 발생하는 원인을 추측해 볼까요? 일단 예제 코드처럼 Intent 전환을 하면 Android는 새롭게 MainActivity 인스턴스를 생성해 처리합니다. 다시 말해 아래와 같은 Navigation Stack이 쌓여 있는 것입니다.

2: MainActivity // StartActivity(mainIntent)로 새로 쌓인 Activity
1: NewActivity  // currentActivity.StartActivity(newIntent)로 쌓인 Activity
0: MainActivity // 최초 App 실행 시 생성된 Activity

그렇다면 2개의 MainActivity 인스턴스가 생성됐고 XAML UI도 각각 새롭게 MainActivity에 붙어야 할 텐데요, 오류 메시지(Window was already created.)에서 알 수 있듯이 이미 한번 생성한 XAML UI를 중복 생성하는 바람에 예외 처리가 된 것입니다.

그래도 저 코드가 .NET 7 이하에서는 잘 동작했다고 하는 것을 보면 그전에는 (동작은 최초 생성한 MainActivity의 상태는 아니었겠지만) 중복 생성을 허용했었던 것을 .NET 8부터는 막은 것이 아닌가 생각됩니다. 물론 저만의 추측인데요, 그렇긴 한데 마이크로소프트는 저 이슈를 별다른 패치 없이 (아래에서 설명할 LaunchMode를 이용하라면서) 그냥 닫아버렸습니다.

그런 면에서 본다면, 엄밀히 MainActivity는 다중 인스턴스로 생성될 의미가 없습니다. 즉, 언제나/항상/무조건 LaunchMode 옵션을 SingleTask로 설정하는 것이 맞습니다.

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density
    , LaunchMode = LaunchMode.SingleTask // 참고: "그림으로 이해하는 Activity LaunchMode 실험"
    )]
public class MainActivity : MauiAppCompatActivity
{
    // ...[생략]...
}

저렇게 바꾸고 나면, 이제야 (OnPostCreate에서의 try/catch를 제거해도) 이 글의 예제에서 작성한 코드들이 아무런 문제 없이 동작하게 됩니다.




참고로, (SingleTask를 사용하지 않고) OnPostCreate에서의 try/catch를 사용하는 우회 방법이 통하지 않는 경우가 있습니다. 바로 MediaElement를 추가했을 때인데요,

<!-- 참고: C# - MAUI에서 MediaElement 사용 -->

<toolkit:MediaElement x:Name="mediaPlayer" />

재미있는 건 저렇게만 사용했을 때는 문제없지만 Event Handler를 걸게 되면,

<toolkit:MediaElement x:Name="mediaPlayer" Loaded="OnMediaLoaded" />

이제부터는 다른 Activity에서 MainActivity로 돌아올 때 이런 예외가 발생합니다.

System.ObjectDisposedException: 'Cannot access a disposed object.
Object name: 'CommunityToolkit.Maui.Core.Views.MauiMediaElement'.'

0xFFFFFFFFFFFFFFFF in Android.Runtime.RuntimeNativeMethods.monodroid_debugger_unhandled_exception   C#
0x1A in Android.Runtime.JNINativeWrapper._unhandled_exception at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:13,5 C#
0x23 in Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPLLL_L at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:368,26    C#
0x2E in Java.Interop.JniPeerMembers.AssertSelf at /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs:153,5    C#
0x1 in Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualVoidMethod at /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods_Invoke.cs:57,5    C#
...[생략]...
0x5A in Microsoft.Maui.Platform.MauiContextExtensions.ToPlatform at D:\a\_work\1\s\src\Core\src\Platform\Android\MauiContextExtensions.cs:96,4  C#
0x2A in Microsoft.Maui.Controls.Platform.Compatibility.ShellFragmentContainer.OnCreateView at D:\a\_work\1\s\src\Controls\src\Core\Compatibility\Handlers\Shell\Android\ShellFragmentContainer.cs:26,4  C#
0x24 in AndroidX.Fragment.App.Fragment.n_OnCreateView_Landroid_view_LayoutInflater_Landroid_view_ViewGroup_Landroid_os_Bundle_ at C:\a\_work\1\s\generated\androidx.fragment.fragment\obj\Release\net6.0-android\generated\src\AndroidX.Fragment.App.Fragment.cs:2031,4 C#
0xD in Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPLLL_L at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:367,5  C#

이번에는 OnPostCreate 단계에서 발생하지 않기 때문에 응용 프로그램은 예외로 인해 종료하게 됩니다. 아마도 이런 문제 때문인지는 알 수 없으나, MediaElement는 애당초 문서에서 LaunchMode를 SingleTask로 지정하라고 명시하고 있습니다.

Platform specific initialization
; https://learn.microsoft.com/en-us/dotnet/communitytoolkit/maui/views/mediaelement?tabs=android#platform-specific-initialization

To initialize the MediaElement on Android, the LaunchMode of the applications Activity must be set to LaunchMode.SingleTask and you must add ResizableActivity=true as per the following example


언젠가는 마이크로소프트가 다중 XAML 초기화를 Activity마다 지원하게 될 수도 있을 것입니다. 적어도 그때까지는, 이런저런 이유로 인해 MainActivity의 LaunchMode는 항상 SingleTask로 지정하는 것이 여러모로 정신 건강에 이로울 듯합니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 6/6/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2024-11-21 09시19분
Android쪽에서 activity 접근 할때 아래꺼 적어 놓고 참고 하고 있습니다. (Maui)
            //var context = Platform.AppContext;
            //Current Activity or null if not initialized or not started.
            //var activity = Platform.CurrentActivity;
            //var activity = await Platform.WaitForActivityAsync();
지현명

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13917정성태4/30/202545VS.NET IDE: 199. Directory.Build.props에 정의한 속성에 대해 Condition 제약으로 값을 변경하는 방법
13916정성태4/23/2025446디버깅 기술: 221. WinDbg 분석 사례 - ASP.NET HttpCookieCollection을 다중 스레드에서 사용할 경우 무한 루프 현상 - 두 번째 이야기
13915정성태4/13/20251652닷넷: 2331. C# - 실행 시에 메서드 가로채기 (.NET 9)파일 다운로드1
13914정성태4/11/20251977디버깅 기술: 220. windbg 분석 사례 - x86 ASP.NET 웹 응용 프로그램의 CPU 100% 현상 (4)
13913정성태4/10/20251204오류 유형: 950. Process Explorer - 64비트 윈도우에서 32비트 프로세스의 덤프를 뜰 때 "Error writing dump file: Access is denied." 오류
13912정성태4/9/2025863닷넷: 2330. C# - 실행 시에 메서드 가로채기 (.NET 5 ~ .NET 8)파일 다운로드1
13911정성태4/8/20251091오류 유형: 949. WinDbg - .NET Core/5+ 응용 프로그램 디버깅 시 sos 확장을 자동으로 로드하지 못하는 문제
13910정성태4/8/20251257디버깅 기술: 219. WinDbg - 명령어 내에서 환경 변수 사용법
13909정성태4/7/20251725닷넷: 2329. C# - 실행 시에 메서드 가로채기 (.NET Framework 4.8)파일 다운로드1
13908정성태4/2/20252138닷넷: 2328. C# - MailKit: SMTP, POP3, IMAP 지원 라이브러리
13907정성태3/29/20251937VS.NET IDE: 198. (OneDrive, Dropbox 등의 공유 디렉터리에 있는) C# 프로젝트의 출력 경로 변경하기
13906정성태3/27/20252191닷넷: 2327. C# - 초기화되지 않은 메모리에 접근하는 버그?파일 다운로드1
13905정성태3/26/20252224Windows: 281. C++ - Windows / Critical Section의 안정화를 위해 도입된 "Keyed Event"파일 다운로드1
13904정성태3/25/20251902디버깅 기술: 218. Windbg로 살펴보는 Win32 Critical Section파일 다운로드1
13903정성태3/24/20251522VS.NET IDE: 197. (OneDrive, Dropbox 등의 공유 디렉터리에 있는) C++ 프로젝트의 출력 경로 변경하기
13902정성태3/24/20251737개발 환경 구성: 742. Oracle - 테스트용 hr 계정 및 데이터 생성파일 다운로드1
13901정성태3/9/20252112Windows: 280. Hyper-V의 3가지 Thread Scheduler (Classic, Core, Root)
13900정성태3/8/20252346스크립트: 72. 파이썬 - SQLAlchemy + oracledb 연동
13899정성태3/7/20251806스크립트: 71. 파이썬 - asyncio의 ContextVar 전달
13898정성태3/5/20252124오류 유형: 948. Visual Studio - Proxy Authentication Required: dotnetfeed.blob.core.windows.net
13897정성태3/5/20252367닷넷: 2326. C# - PowerShell과 연동하는 방법 (두 번째 이야기)파일 다운로드1
13896정성태3/5/20252180Windows: 279. Hyper-V Manager - VM 목록의 CPU Usage 항목이 항상 0%로 나오는 문제
13895정성태3/4/20252220Linux: 117. eBPF / bpf2go - Map에 추가된 요소의 개수를 확인하는 방법
13894정성태2/28/20252251Linux: 116. eBPF / bpf2go - BTF Style Maps 정의 구문과 데이터 정렬 문제
13893정성태2/27/20252197Linux: 115. eBPF (bpf2go) - ARRAY / HASH map 기본 사용법
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...